Today I have found this question on forums.asp.net and decided to take a look at it. I must warn you, the solution might be considered a little bit hacky.
So for starters, we have an ActionLink which looks like this:
<%= Ajax.ActionLink("Delete", "AjaxDeleteItem", new { ItemId = item.ItemId }, new AjaxOptions { Confirm = "Are you sure you want to delete the item?", HttpMethod = "Delete", UpdateTargetId = "divList"}) %>
and we want to secure this Delete action with AntiForgeryToken. First we need to add AntiForgeryToken helper to the view:
<%= Html.AntiForgeryToken() %>
Now let's modify our ActionLink:
<%= Ajax.ActionLink("Delete", "AjaxDeleteItem", new { ItemId = item.ItemId, __RequestVerificationToken = "_" }, new AjaxOptions { Confirm = "Are you sure you want to delete the item?", HttpMethod = "Delete", UpdateTargetId = "divList" })%>
Here comes the hacky part. We will use this additional route value (__RequestVerificationToken = "_") as a placeholder to inject the value of our AntiForgeryToken into links href with a little help of jQuery:
$(document).ready(function () {
//Finding AntiForgeryToken input
var antiForgeryToken = $('input[name=__RequestVerificationToken]');
if (antiForgeryToken.length > 0) {
//Serializing AntiForgeryToken
var antiForgeryTokenSerialized = antiForgeryToken.serialize();
//For each anchor in page
$('a').each(function (index, element) {
//Replace placeholder with serialized AntiForgeryToken
$(element).attr('href', $(element).attr('href').replace('__RequestVerificationToken=_', antiForgeryTokenSerialized));
});
}
});
Our job on client side is done, all the links which had our placeholder in their href now have AntiForgeryToken. We should now add token verification to the controller action:
[AcceptVerbs(HttpVerbs.Delete)]
[ValidateAntiForgeryToken]
public ActionResult AjaxDeleteItem(string ItemId)
{
...
}
If you would run the code now, you would see that it's not working. That's because AntiForgeryToken is designed for POST requests and it expects the token in Request.Form collection. To work around this we will modify the following line of OnAuthorization method in ValidateAntiForgeryTokenAttribute class:
string formValue = filterContext.HttpContext.Request.Form[fieldName];
to this:
HttpVerbs verb;
//Parse HttpVerb from Request.GetHttpMethodOverride()
if (!Enum.TryParse<HttpVerbs>(filterContext.HttpContext.Request.GetHttpMethodOverride(), true, out verb))
//If method couldn't be parsed, assume POST
verb = HttpVerbs.Post;

//Extract token based on HttpVerb
string formValue = String.Empty;
if (verb == HttpVerbs.Post || verb == HttpVerbs.Put)
formValue = filterContext.HttpContext.Request.Form[fieldName];
else
formValue = filterContext.HttpContext.Request.Params[fieldName];
After that small change, this workaround works the way we wanted to. The code was tested only in few simple testcases, so it migth contain some flaws.

This time I'm going to write about using jQuery Autocomplete plugin in ASP.NET MVC. If we want to use the plugin, we should reference following JavaScript and CSS:
<link href="<%= Url.Content("~/Content/jquery.autocomplete.css") %>" rel="stylesheet" type="text/css" />
<script src="<%= Url.Content("~/Scripts/jquery-1.4.1.min.js") %>" type="text/javascript"></script>
<script src="<%= Url.Content("~/Scripts/jquery.autocomplete.min.js") %>" type="text/javascript"></script>
We will start by creating simple autocomplete with static data (below JavaScript assumes that there is already a input with type text and id region):
//Static data for autocomplete
var regions = ['Eastern', 'Western', 'Northern', 'Southern'];

$(document).ready(function () {
//Adding autocomplete to region input
$("#region").autocomplete(regions, {
//Minimum number of characters a user has to type before the autocompleter activates
minChars: 0,
//The number of items in the select box
max: 4,
//Fill the input while still selecting a value
autoFill: true,
//Only suggested values are valid
mustMatch: true,
//The comparison doesn't looks inside
matchContains: false
});
});
Now we will extend regions so it contains objects with regionId and regionDescription properties. We want to display the regionDescription in autocomplete and put selected regionId into input of type hidden. To achieve this, we will use autocomplete callbacks for formatting data and result. But first let's change markup:
<%: Html.Hidden("regionId", -1) %>
<label for="regionDescription">Region:</label> <%: Html.TextBox("regionDescription")%>
And here is modified JavaScript:
//Static data for autocomplete
var regions = [
{ regionId: 1, regionDescription: 'Eastern' },
{ regionId: 2, regionDescription: 'Western' },
{ regionId: 3, regionDescription: 'Northern' },
{ regionId: 4, regionDescription: 'Southern' }
];

$(document).ready(function () {
//Adding autocomplete to region input
$("#regionDescription").autocomplete(regions, {
//Minimum number of characters a user has to type before the autocompleter activates
minChars: 0,
//The number of items in the select box
max: 4,
//Fill the input while still selecting a value
autoFill: true,
//Only suggested values are valid
mustMatch: true,
//The comparison doesn't looks inside
matchContains: false,
//Callback which will format items for autocomplete list
formatItem: function (data, index, max) {
//Display only descriptions in list
return data.regionDescription;
},
//Callback which will format items for matching
formatMatch: function (data, index, max) {
//Match only descriptions
return data.regionDescription;
},
//Callback which will format result
formatResult: function (data, index, max) {
//Display only description as result
return data.regionDescription;
}
//Callback which will be called when user chooses value
}).result(function (event, data, formatted) {
//Set region id as hidden input value
if (data)
$("#regionId").val(data.regionId);
else
$("#regionId").val('-1');
});
});
What if we want to achieve the same behavior, but with remotely loaded data? We should start by adding markup for new inputs:
<%: Html.Hidden("territoryId", -1)%>
<label for="territoryDescription">Territory:</label> <%: Html.TextBox("territoryDescription")%>
The JavaScript isn't that much different, we just need to pass URL instead of array with data (you can also adjust local cache size):
//Adding autocomplete to territoryDescription input
$("#territoryDescription").autocomplete('<%: Url.Action("Territories", "Home") %>', {
//Don't fill the input while still selecting a value
autoFill: false,
//Only suggested values are valid
mustMatch: true,
//The comparison doesn't looks inside
matchContains: false,
//The number of query results to store in cache
cacheLength: 12,
//Callback wich will format items for autocomplete list
formatItem: function (data, index, max) {
//Display only descriptions in list
return data[1];
},
//Callback which will format items for matching
formatMatch: function (data, index, max) {
//Match only descriptions
return data[1];
},
//Callback which will format result
formatResult: function (data, index, max) {
//Display only description as result
return data[1];
}
//Callback which will be called when user chooses value
}).result(function (event, data, formatted) {
//Set territory id as hidden input value
if (data)
$("#territoryId").val(data[0]);
else
$("#territoryId").val('-1');
});
Controller action will receive three parameters: q - the string we should search for, limit - maximum number of items we should return (equal to max option) and timestamp. The plugin is expecting quite unusual response. It should be a string, where '\n' is a row delimeter and '|' is cells separator. So the action will look like this (I will skip database access code):
public ContentResult Territories(string q, int limit, Int64 timestamp)
{
StringBuilder responseContentBuilder = new StringBuilder();
IQueryable<Territory> territories = _repository.GetTerritories(q, limit);
foreach (Territory territory in territories)
responseContentBuilder.Append(String.Format("{0}|{1}\n", territory.TerritoryID, territory.TerritoryDescription));

return Content(responseContentBuilder.ToString());
}
So our autocomplete is now populated from server. Lets take it one step further, and make territory dependant on region. We will use extraParams for this:
//Adding autocomplete to territoryDescription input
$("#territoryDescription").autocomplete('<%: Url.Action("Territories", "Home") %>', {
//Don't fill the input while still selecting a value
autoFill: false,
//Only suggested values are valid
mustMatch: true,
//The comparison doesn't looks inside
matchContains: false,
//The number of query results to store in cache
cacheLength: 12,
//Extra parameters which will be put into query string
extraParams: {
regionId: function () { return $("#regionId").val(); }
},
//Callback which will format items for autocomplete list
formatItem: function (data, index, max) {
//Display only descriptions in list
return data[1];
},
//Callback which will format items for matching
formatMatch: function (data, index, max) {
//Match only descriptions
return data[1];
},
//Callback which will format result
formatResult: function (data, index, max) {
//Display only description as result
return data[1];
}
//Callback which will be called when user chooses value
}).result(function (event, data, formatted) {
//Set territory id as hidden input value
if (data)
$("#territoryId").val(data[0]);
else
$("#territoryId").val('-1')
});
After that change we will receive on more parameter in our action:
public ContentResult Territories(string q, int regionId, int limit, Int64 timestamp)
{
StringBuilder responseContentBuilder = new StringBuilder();
IQueryable<Territory> territories = _repository.GetTerritories(regionId, q, limit);
foreach (Territory territory in territories)
responseContentBuilder.Append(String.Format("{0}|{1}\n", territory.TerritoryID, territory.TerritoryDescription));

return Content(responseContentBuilder.ToString());
}
This way we have finished our simple sample. There are few more interesting options in this plugin (for example you can attach it to textarea and/or allow multiply selection). Our sample application looks like this:


You can download source code from here.

Some time ago I have written about using jQuery TreeView plugin for creating an asynchronous TreeView in ASP.NET MVC. Recently I had to use the same plugin in WebForms application, so I decided to share my experience. First let's reference all JavaScript and CSS needed:
<script src="Scripts/jquery-1.4.1.min.js" type="text/javascript"></script>
<script src="Scripts/jquery.treeview.min.js" type="text/javascript"></script>
<script src="Scripts/jquery.treeview.async.js" type="text/javascript"></script>
<link href="~/Styles/jquery.treeview.css" rel="stylesheet" type="text/css" />
We should also add the markup for TreeView (an empty list):
<ul id="trFileBrowser"></ul>
Now we should provide data for TreeView, there are generally two approaches in which we can do that.
WebService approach
If we want to use WebService as the data source, we should start by adding an Ajax-enabled WCF Service to the project:
[ServiceContract(Namespace = "AsyncTreeViewExample")]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class TreeView
{
...
}
and enabling the Web programming model for this service (in web.config):

...
<system.serviceModel>
<behaviors>
<endpointBehaviors>
<behavior name="AsyncTreeViewExample.TreeViewAspNetAjaxBehavior">
<webHttp />
</behavior>
</endpointBehaviors>
</behaviors>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" />
<services>
<service name="AsyncTreeViewExample.TreeView">
<endpoint address="" behaviorConfiguration="AsyncTreeViewExample.TreeViewAspNetAjaxBehavior" binding="webHttpBinding" contract="AsyncTreeViewExample.TreeView" />
</service>
</services>
</system.serviceModel>
...
In order to write WebService method properly, we need to remember that plugin performs GET request with application/x-www-form-urlencoded content type and expects bare JSON serialized array in response. Knowing all that, the method should look like this (TreeViewNode class was described in previous post about this plugin):

[OperationContract]
//Accept GET request and return bare JSON serialized result
[WebInvoke(Method = "GET", BodyStyle = WebMessageBodyStyle.Bare, ResponseFormat = WebMessageFormat.Json)]
public List<TreeViewNode> FileBrowserData(string root)
{
DirectoryInfo rootDirectory = null;
if (root == "source")
rootDirectory = new DirectoryInfo(@"E:\");
else
rootDirectory = new DirectoryInfo(root);

var directoryChildren = from child in rootDirectory.GetFileSystemInfos()
orderby child is DirectoryInfo descending
select child;

List<TreeViewNode> nodes = new List<TreeViewNode>();
foreach (FileSystemInfo directoryChild in directoryChildren)
{
bool isDirectory = directoryChild is DirectoryInfo;
nodes.Add(new TreeViewNode()
{
id = directoryChild.FullName,
text = directoryChild.Name,
classes = isDirectory ? "folder" : "file",
hasChildren = isDirectory
});
}

return nodes;
}
Last thing to do in this approach is adding some JavaScript for initialization purposes:
<script type="text/javascript">
$(document).ready(function () {
$('#trFileBrowser').treeview({
url: '<% = ResolveClientUrl("~/TreeView.svc/FileBrowserData") %>'
});
});
</script>
PageMethod approach
Using PageMethod as a source of data is a little bit more tricky. First of all PageMethods requires request to have application/json content type (so all the parameters have to be properly JSON serialized). Also PageMethod always returns response with wrapped body. There are three ways in which we can workaround this.
First way is to modify the plugin in such a way that it will work with PageMethod requirements (I would recommend this way, as others should be considered nasty workarounds). This is modified load funtion from jquery.treeview.async.js (I'm also changing request from GET to POST here):
function load(settings, root, child, container) {
$.ajax({
//Changing request to POST
type: "POST",
url: settings.url,
//Required content type
contentType: 'application/json; charset=utf-8',
//Properly JSON serialized data
data: '{"root": "' + root + '"}',
dataType: 'json',
success: function(response) {
function createNode(parent) {
var current = $("<li/>").attr("id", this.id || "").html("<span>" + this.text + "</span>").appendTo(parent);
if (this.classes) {
current.children("span").addClass(this.classes);
}
if (this.expanded) {
current.addClass("open");
}
if (this.hasChildren || this.children && this.children.length) {
var branch = $("<ul/>").appendTo(current);
if (this.hasChildren) {
current.addClass("hasChildren");
createNode.call({
text:"placeholder",
id:"placeholder",
children:[]
}, branch);
}
if (this.children && this.children.length) {
$.each(this.children, createNode, [branch])
}
}
}
//Accessing wrapped array
$.each(response.d, createNode, [child]);
$(container).treeview({add: child});
}
});
}
After that we just need a regular PageMethod:
[System.Web.Services.WebMethod]
public static List<TreeViewNode> FileBrowserData(string root)
{
//Method body is the same as in WebService
...
}
and initalization JavaScript:
<script type="text/javascript">
$(document).ready(function () {
$('#trFileBrowser').treeview({
url: '<% = ResolveClientUrl("~/PageMethodPlugin.aspx/FileBrowserData") %>'
});
});
</script>
Second way we can call a 'client-side workaround'. The idea is to change some default settings for all Ajax requests (it's nasty and I wouldn't recommend using it):

<script type="text/javascript">
$(document).ready(function () {
//Default callback for creating the XMLHttpRequest object
var xhr = $.ajaxSettings.xhr;

//Changing default values for all Ajax requests
$.ajaxSetup({
//Required content type
contentType: 'application/json; charset=utf-8',
//New callback for creating the XMLHttpRequest object
xhr: function () {
//Wrapping url parameters with '"'
if (this.url.indexOf('=') >= 0) {
this.url = this.url.replace(/=/g, '="') + '"';
}
//Calling default callback
return xhr.call(this);
},
//Sanitize the response (remove wrapping)
dataFilter: function (data, dataType) {
if (dataType == "json") {
var result = $.parseJSON(data);
if (result.d) {
return result.d;
} else {
return data;
}
} else {
return data;
}
}
});
$('#trFileBrowser').treeview({
url: '<% = ResolveClientUrl("~/PageMethodClient.aspx/FileBrowserData") %>'
});
});
</script>
We also have to allow GET verb on PageMethod:

[System.Web.Services.WebMethod]
[System.Web.Script.Services.ScriptMethod(UseHttpGet = true)]
public static List<TreeViewNode> FileBrowserData(string root)
{
//Method body is the same as in WebService
...
}
Third way is 'server-side workaround'. We will write PageMethod in a little bit different way, so it will work with TreeView:
[System.Web.Services.WebMethod]
[System.Web.Script.Services.ScriptMethod(UseHttpGet = true)]
//Method doesn't take any parameters and return anything
public static void FileBrowserData()
{
//We are getting 'root' directly from request params
string root = HttpContext.Current.Request.Params.Get("root");

//Nodes generation code is the same as in other cases
...

//We are serializing nodes
JavaScriptSerializer serializer = new JavaScriptSerializer();
string nodesSerialized = serializer.Serialize(nodes);

//And writing them directly into response
HttpContext.Current.Response.Clear();
HttpContext.Current.Response.ContentType = "application/json; charset=utf-8";
HttpContext.Current.Response.AddHeader("Content-Length", nodesSerialized.Length.ToString());
HttpContext.Current.Response.AddHeader("Connection", "Close");
HttpContext.Current.Response.Write(nodesSerialized);
HttpContext.Current.Response.Flush();
}
What is left is few lines of JavaScript:
<script type="text/javascript">
$(document).ready(function () {
//Changing default values for all Ajax requests
$.ajaxSetup({
//Required content type
contentType: 'application/json; charset=utf-8'
});
$('#trFileBrowser').treeview({
url: '<% = ResolveClientUrl("~/PageMethodClient.aspx/FileBrowserData") %>'
});
});
</script>
Now we have all approaches covered, the result of this work looks like this:
You can download source code from repository.
Older Posts