Recently there was an issue posted to the ASP.NET customer forums where somebody was having problems using Profile Services with ASP.NET Ajax Preview 5. The reason is that the scripts for Application Services have been moved to a separate script file (to improve script download size). We did not include the script in the Preview 5 release however, so if you want to use Preview 5 with Application Services features, you need to work around by taking the Sys.Services code and merging it into the Preview 5 MicrosoftAjax.js file. For your convenience and at Dave Reed’s suggestion, I’ve gone ahead and done this, so if you are targeting this scenario, use the new versions of MicrosoftAjax.js and MicrosoftAjax.debug.js that are included in the zip file attached to this post.
Many of you have probably heard that we’ve released ASP.NET Ajax Preview 5 on Codeplex, and it’s available here. Aside from all the cool updates to the codebase, Preview 5 also includes some updated samples, as well as support for UpdatePanel when using ASP.NET 3.5 SP1.
Previously, this didn’t work because of updates to the scripts for compatibility with 4.0. Now, with this fixed, you can easily add Ajax Preview 5 functionality to existing sites and enjoy continued operation. There is a very simple example included with the Preview 5 samples that demonstrates this functionality (8_UpdatePanel.aspx under the 1_Basic_DataView folder). I’ll quickly cover here how to get your existing UpdatePanel working with the new Preview bits.
Basically, all you need to have is an additional ScriptReference to the included MicrosoftAjaxWebForms.js file for Preview 5 to work with the UpdatePanel. So your ScriptManager should look something like this:
<asp:ScriptManager ID="sm1" runat="server">
<Scripts>
<asp:ScriptReference Name="MicrosoftAjax.js" Path="~/MicrosoftAjax/MicrosoftAjax.js"/>
<asp:ScriptReference Name="MicrosoftAjaxWebForms.js" Path="~/MicrosoftAjax/MicrosoftAjaxWebForms.js" />
</Scripts>
</asp:ScriptManager>
This will allow you use any UpdatePanels that you have on the page in exactly the same way you did in 3.5 SP1, while providing the flexibility for you to include other Ajax Preview scripts and start using those features side-by-side.
I would argue one step further, however, and state that in many cases, where you were using an UpdatePanel before, you can now move to using ADO.NET web services coupled with the Preview 5 scripts.
To illustrate this, let’s take a look at an old school sample using UpdatePanel and GridView. This sample illustrates using the UpdatePanel and GridViews to create a simple read-only employee name entry system. A screenshot is shown below:
We’re going to put this sample to shame using Preview 5.
There’s 146 lines of markup in this page, and every time you hit “Insert”, you’re looking at a partial-page postback, which has to hit the server to do processing, pull down the data for the new page, and then update the appropriate portions.
If instead we use a DataView hooked up to an ADO.NET data context, we can build a similar application which will be much more efficient (dealing with JSON instead of full sets of page data on the wire), much shorter, and much simpler. Let’s begin.
Since we already have the samples, let’s create a new .aspx page, Employees.aspx, under the 1_Basic_DataView folder. Let’s set up the following ScriptManager:
<asp:ScriptManager ID="sm1" runat="server">
<Scripts>
<asp:ScriptReference Name="MicrosoftAjax.js" Path="~/MicrosoftAjax/MicrosoftAjax.js"/>
<asp:ScriptReference Name="MicrosoftAjaxWebForms.js" Path="~/MicrosoftAjax/MicrosoftAjaxWebForms.js" />
<asp:ScriptReference Path="~/MicrosoftAjax/MicrosoftAjaxTemplates.js" ScriptMode="Inherit" />
<asp:ScriptReference Path="~/MicrosoftAjax/MicrosoftAjaxDataContext.js" ScriptMode="Inherit" />
<asp:ScriptReference Path="~/MicrosoftAjax/MicrosoftAjaxAdoNet.js" ScriptMode="Inherit" />
</Scripts>
</asp:ScriptManager>
Users of previous previews might recognize that there is a new file here, MicrosoftAjaxDataContext.js, which now contains the DataContext and AdoNetDataContext classes. Of course, these classes become more useful in read/write scenarios, but I’m going to use them in this read-only example for illustration purposes.
Let’s also take this opportunity to set up our <body> tag for DataView use by adding the appropriate namespaces.
<body xmlns:sys="javascript:Sys" xmlns:dv="javascript:Sys.UI.DataView">
Also remember to add the .sys-template style to your <head> section:
<style type="text/css">
.sys-template {display:none}
</style>
So now we’re ready to add our AdoNetDataContext. Let’s set up our pointer to the service:
<script type="text/javascript">
var myDC = new Sys.Data.AdoNetDataContext();
myDC.set_serviceUri("../Services/ImagesDataService.svc");
</script>
So here I’ve created a new AdoNetDataContext and pointed its serviceUri to my ADO.NET Data Service. Now I’m going to set up a DataView to query this service for People so that I can get a list. So I enter the following markup:
<div id="inputTable">
First Name: <input id="firstNameInput" type="text" /><br />
Last Name: <input id="lastNameInput" type="text" /><br />
<a href="#" onclick="insertPerson()">Insert</a>
<a href="#" onclick="cancelPerson()">Cancel</a>
</div>
<br />Employees:<br />
<div id="employeeView" class="sys-template" sys:attach="dv"
dv:dataprovider="{{myDC}}" dv:autofetch="true" dv:fetchoperation="People">
{{FirstName}} {{LastName}}<br />
</div>
So here I’m setting my DataView’s dataprovider to the AdoNetDataContext that I created, turning on autofetch, and specifying the fetchoperation to query for “People” from the database. I’ve also set up a simple UI which includes links to the insertPerson() and cancelPerson() JS functions, which I’m going to write now:
function insertPerson() {
var firstName = $get("firstNameInput").value;
var lastName = $get("lastNameInput").value;
if (firstName != "" && lastName != "") {
var myObject = {
FirstName: firstName,
LastName: lastName
}
myDC.insertEntity(myObject, "People");
var data = $find("employeeView").get_data();
Sys.Observer.insert(data, data.length, myObject);
$get("firstNameInput").value = "";
$get("lastNameInput").value = "";
} else {
alert("You must enter a first and last name!");
}
}
function cancelPerson() {
$get("firstNameInput").value = "";
$get("lastNameInput").value = "";
}
So basically in insertPerson(), I’m creating a person object based on the first and last name that were entered by the user (assuming they weren’t blank), and inserting an entity into my AdoNetDataContext. For the purposes of this example, this isn’t strictly necessary, but I do it here for illustration purposes (in case you want to add read/write later). Then, I simply need to update the rendered data on the client, and I do so using the insert method of the Sys.Observer class, which allows me to insert the person object in a way that is recognized by the DataView. Then I clear the input fields for the next person to be entered. In cancelPerson(), I’m doing something similar, where I simply clear the input fields.
Of course, it’s easy to add read/write scenarios to this sample. I encourage you to check out the ImageOrganizer sample and associated code there for further examples.
So there you have it. Although it doesn’t do exactly what the UpdatePanel sample does, it’s essentially the same idea. Final line count: 67. Win :)
The kind folks over at Coding QA Podcast (www.codingqa.com) did an interview with me yesterday, where I talk about some of the challenges with testing and building Microsoft AJAX and talk a little bit about myself. There’s also a brief discussion of the Lightweight Test Automation Framework, and the new IE8 Debugger. I forget what the questions are every so often, but in my unbiased opinion it’s a pretty good listen ;). Click here to jump to the Coding QA Podcast site.
Politian has posted a lot of samples on using the new ADO.NET Data Services features in ASP.NET AJAX Preview 4 (in fact, at the time of this posting, the entire blog is dedicated to Preview 4!). He walks through some basic scenarios, but then goes into detail about interesting sample topics as he builds his MIX Presenter Application. Highlights thus far:
Keep up the great work Politian! Remember that you can get ASP.NET AJAX 4.0 Preview 4 on Codeplex.
10-4 and Jonathan Carter just did a great episode where they walk through creating a sample with ADO.NET Data Services, and showcase some of the things that have changed since we released ASP.NET AJAX Preview 3. There's some great coverage on DataView, AdoNetDataContext (formerly AdoNetDataSource), and bindings. Jonathan also goes through command bubbling and item selection integration with master-detail DataViews. The cool thing is that Jonathan uses imperative code, as opposed to the declarative syntax that I've gone over before in my ADO.NET post. The choice is largely a preference, but both methods are perfectly valid. The video is about 27 minutes long, but is well worth it. Silverlight is required, click here for the video.
Note that the demo was created using preview bits that are not yet available on CodePlex, but are coming soon in a future CodePlex release. For now, aside from the new DataContext stuff, most of what is in the video can be accomplished with the ASP.NET AJAX Preview 3, available here.
I hope everybody had a great holiday break and is ready to go for 2009 :).
In .NET 3.5 SP1 we shipped some improvements to the AJAX framework, including the AJAX History Feature, which allows you to add history points in the browser for AJAX applications. There have been some great blog posts about how to use the history feature; Jonathan Carter has a post specifically talking about it client-side here.
Jonathan's sample is clean and simple, and showcases the feature well. He includes a lot of great detail on the intricacies and inner workings of AJAX client-side history. I'm actually going to refer to his post a lot as I build a similar demo, making use of some of the cool new features in ASP.NET AJAX Preview 3. If you are unfamiliar with the history feature, now would be a good time to read through the first two parts of Jonathan's series on ASP.NET AJAX History here and here, as I'm going to assume some basic knowledge about the feature.
To start off my sample site, I've done a lot of the same setup steps as my last app: basically I've created a new web application and added the ASP.NET AJAX Preview 3 scripts into the project. If you're unsure how to get up until that point, please refer to this post and follow the instructions under "Website Setup".
One difference is that my ScriptManager doesn't need the MicrosoftAjaxAdoNet.js file since I won't be using data services in this app. and also that my ScriptManager now needs to enable the AJAX History scripts by setting EnableHistory="true".
<asp:ScriptManager ID="ScriptManager1" EnableHistory="true" runat="server">
<Scripts>
<asp:ScriptReference Path="~/Scripts/MicrosoftAjaxTemplates.js" />
</Scripts>
</asp:ScriptManager>
If you've read through Jonathan's post, you'll note that he's done some work to get his selected item concept working. With ASP.NET AJAX Preview 3, we get this functionality for free through the DataView, so let's go ahead and set that up now.
I'm using a default StyleSheet, Default.css, which is available in the zip file of the project here. I've also added some classes for the body and selectedItem which we will use later. So my style section looks like this:
<style type="text/css">
@import url(Default.css);
.sys-template { display:none }
body
{
width: 350px;
margin: 0px auto;
}
.selectedItem
{
text-decoration:none;
color:White;
background-color:Black;
}
</style>
I'm also going to go ahead and set up some JavaScript data at this point so I can have an array to bind to when we set up the dataview. Here I've created a list of people, using members of the AJAX team.
var myList = [{ name: "Jim Wang", location: "Vancouver" },
{ name: "Dave Reed", location: "Los Angeles" },
{ name: "Clay Compton", location: "Kirkland" },
{ name: "Nghi Nguyen", location: "Redmond" },
{ name: "Bertrand Le Roy", location: "Seattle" },
{ name: "Boris Moore", location: "Bellevue" }];
So now I can set up my master and detail view in declarative markup, where I show the name of the person in the master, and display the location in the detail. Here's the master view:
<fieldset>
<legend>Person</legend>
<div id="personListMaster" sys:attach="dv"
dv:data="{{myList}}"
dv:initialselectedindex="-1"
dv:selecteditemclass="selectedItem"
dv:sys-key="master" >
<a sys:command="select">{{name}}</a><br />
</div>
</fieldset>
I've disabled the default selected index of 0 (and set it to -1 declaratively) so that when the page renders, there will be no item selected initially. I've also added the selectedItem class declaratively, and set a sys-key on the master, so I can refer to it in the detail view without doing a $find. Here's the detail view:
<fieldset>
<legend>Location</legend>
<div id="personListDetail" sys:attach="dv"
dv:data="{binding selectedData, source={{master}}}">
<span>{{location}}</span>
</div>
</fieldset>
As you can see, I'm able to bind to the selectedData property of the master simply by referring to it by its sys-key, which is cleaner than doing a $find.
So at this point the site should be operational: you can load it up and click on a name to populate the detail view like so:
Now that we have the selection of names working, let's add the history feature. The first thing I'm going to do is add a command handler to my master view. If you look at the master view, you'll see that the link has the sys:command="select" property associated with it. This command is special, it means that when you click on that item, it will become selected and the selected item class, if available, will be applied. However, we can also add to this functionality by adding a handler for the event, and passing in a command argument, which we will do for the purposes of this demo. So my new master view looks like this:
<fieldset>
<legend>Person</legend>
<div id="personListMaster" sys:attach="dv"
dv:data="{{myList}}"
dv:initialselectedindex="-1"
dv:selecteditemclass="selectedItem"
dv:sys-key="master"
dv:oncommand="{{personClicked}}">
<a sys:command="select" sys:commandargument="{{$index}}">{{name}}</a><br/>
</div>
</fieldset>
So I've added a handler called personClicked (which I'm going to have to define in my JavaScript), and I'm passing a commandargument of $index. $index is a special pseudo-column, accessible within a template/dataview, that gives the current index of the item being rendered, starting from 0. So in my case, when this dataview is rendered, since I have 6 names, I will generate $index values of 0 to 5.
With this new code, when I click on a name, it is selected, bubbles the command up to the dataview, and then the personClicked handler is called and passed an argument of $index, which will vary depending on which name was clicked (which is exactly what we want, since we need to be able to tell which person was clicked).
Here's the code for personClicked, which is where we will add our history point.
function personClicked(sender, args) {
if (args.get_commandName() === "select") {
var mySelectedIndex = args.get_commandArgument();
var myTitle = myList[mySelectedIndex].name;
Sys.Application.addHistoryPoint({selected : mySelectedIndex}, myTitle);
}
}
So here, I'm ensuring that the command is a "select" before doing further processing. In general, you can make up your own command names, so I could specify sys:command="myFoo" to an element, and then, if I specified a command handler, I would be able to pick out that command using the get_commandName() API. However, in this case, I'm only interested in "select". After I've verified that the event was a select, I pull out the command argument, which tells me the index of the person who was clicked on. Since I'm dealing with a fixed array, I can immediately get the name of the person associated with that index by looking at the array. Finally, I can add my history point with these parameters. I've arbitrarily named the property "selected", because all I need to save in state is the currently selected index. Also note that the user can see this property in the URL, so making it compact and meaningful is important. I'm also passing in the optional title property to change the title of the website (and history entry) to the name of the person who has been clicked.
This might be a good time to note that although passing the $index pseudo-column in as a command argument is one way to get the selected index in the personClicked handler, you can also access the same information directly from the sender and args like so:
mySelectedIndex = sender.getItem(args.get_commandSource()).index;
In this case, the sender is the DataView, so we call the getItem API on the source of the command (which is the element being clicked), and look at the index property. So using this method, although slightly more complex, eliminates the need to pass a command argument to the handler, but I'm going to leave the command argument implementation as is for illustration purposes.
If I didn't want to use a command handler, I could also hook into the selectedIndex change event, enabling me to pick up changes in the selected index from things other than the user clicking on an item (for example, if I was to change the selected index in code using the DataView's set_selectedIndex() API). For simplicity and illustration purposes, I'm going to leave the command handler implementation as is, but if you wanted to hook into the selectedIndex change event, you could use the following code to add a property changed handler to the dataview, and then add the history point:
function pageLoad() {
$find("personListMaster").add_propertyChanged(myHandler);
}
function myHandler(sender, args) {
if (args.get_propertyName() == "selectedIndex") {
var mySelectedIndex = $find("personListMaster").get_selectedIndex();
var myTitle = myList[mySelectedIndex].name;
Sys.Application.addHistoryPoint({selected: mySelectedIndex}, myTitle);
}
}
Now, all that is left to do is add a handler for the Navigate event so that the page knows what to do with the history points. We can do this in init like so (put this snippet right after the ScriptManager, otherwise you won't be able to resolve the Sys namespace):
<script type="text/javascript">
Sys.Application.add_init(function() {
Sys.Application.add_navigate(navigatePerson);
});
</script>
With the associated navigatePerson function defined as follows:
function navigatePerson(sender, args) {
var clickedSelectedIndex = args.get_state().selected;
$find("personListMaster").set_selectedIndex(clickedSelectedIndex);
document.title = clickedSelectedIndex ?
myList[clickedSelectedIndex].name : "Ajax History Blog Post";
}
Note that here I'm pulling only the index out of the state information, and then using that to directly set the selectedIndex of the dataview. I'm also restoring the old title information by setting document.title to the name of the person if the clickedSelectedIndex is defined, and setting it back to the default title if it isn't defined. So now if we load up the application, we should have full history support, being able to click on different people, and then using the browser's back button to jump back to previous states. One cool thing is that the detail view data is also seamlessly saved (which makes sense since we only depend on the selected index, and the detail view is live-bound to the selected data of the master). A typical URI looks like this:
http://localhost:18142/AjaxHistory/Default.aspx#selected=5
Which is as expected. Note that from a security standpoint, I can create my own user states by simply changing the last number in the URI, which is why you shouldn't store any critical information in unencrypted client-side AJAX history. In our case, this isn't a risk because all we're doing is changing the selected item.
A final note is that in this simple example, I'm not changing the array of items at all, so my selected index is always going to be consistent. However, if I were to add the ability to add and remove items from the array (perhaps using Sys.Observer), this example is going to break, because when I modify the collection, I change the selected index values. So for example, if I had Dave selected, but I removed Jim, Dave's selected index goes from being 1 to 0, which means that I cannot accurately reconstruct the state of the page using selected index alone, and will need a more complex data key (preferably a primary key) to reconstruct the user's state. However, for this simple example, using the selected index is lightweight and works fine.
I hope this post has given you some good ideas on how to use the new ASP.NET AJAX stuff to do creative things with older legacy features. The big things that stick out for me are that we didn't have to loop through our elements and add handlers because of the command bubbling architecture of dataview, and that we didn't have to write any code to get selected items working. The full website is attached as a zip to this post. Happy Coding!
Introduction
If you haven't heard, we've shipped ASP.NET AJAX CodePlex Preview 3. It's live and available here. I'm really excited about this release because it includes client-side support for ADO.NET Data Services, replacing the AJAX Client Library for ADO.NET Data Services that we had on CodePlex before.
The new library currently has read-only declarative integration with the DataView, but on its own, includes support for query, insert, update, and remove operations. We also support optimistic concurrency and POST verb tunneling.
In this blog post, I'm going to walk through building a simple ASP.NET Web Site that showcases using ADO.NET Data Services with our new client library, while also using familiar features from ASP.NET AJAX CodePlex Preview 2, such as the DataView and Client-Side Databinding. I'll also include a simple master-detail view with support for DataView placeholders and the concept of a selected item (new features in Preview 3).
I built this demo using Visual Studio 2008 Team Suite SP1. You will need a version of Visual Studio with SP1 in order for the demo to work properly, and you will also need to have the AJAX CodePlex Preview 3 bits, which you can download here. Note that certain dialogs may appear differently if you are using a different version of Visual Studio than I am. I'm also going to assume some level of familiarity with the features we shipped in AJAX Preview 2.
The finished product will be a website that allows you to manipulate an ADO.NET Data Service from JavaScript, including insert, delete, and update operations. It may help you before you start to download the zipped up project (AdoNetDemo.zip) attached to this post to get a feel for what the final product is like.
Website Setup
Let's start by creating a blank website. Open Visual Studio and navigate to File->New Website. I'm going to create my site in my Documents folder and call it AdoNetDemo.
You should now have a website set up with an App_Data folder, web.config, and Default.aspx/Default.aspx.cs. Let's go ahead and make the following changes to the head section of Default.aspx:
<head runat="server">
<title>AdoNetDemo</title>
<style type="text/css">
@import url(Default.css);
.sys-template { display:none }
</style>
</head>
Here we're adding the sys-template class to support the DataView features we'll be using later on. I've also imported a default stylesheet to clean up the appearance of the site, feel free to substitute your own (mine, Default.css, is available in this zip). Now, let's copy over MicrosoftAjaxAdoNet.js and MicrosoftAjaxTemplates.js to a Scripts sub-folder in the website (don't forget to create the Scripts folder through the designer), and add the files to the project as existing items by right clicking on the Scripts folder, as shown below.
Now we can add the references to the client libraries, as well as namespace support for the DataView, as shown by the following code snippets for the body:
<body xmlns:sys="javascript:Sys"
xmlns:dv="javascript:Sys.UI.DataView"
sys:activate="*">
and for the ScriptManager (inside the form tag):
<asp:ScriptManager ID="ScriptManager1" runat="server">
<Scripts>
<asp:ScriptReference Path="~/Scripts/MicrosoftAjaxAdoNet.js" />
<asp:ScriptReference Path="~/Scripts/MicrosoftAjaxTemplates.js" />
</Scripts>
</asp:ScriptManager>
ADO.NET Data Service Setup
Now let's go ahead and set up our ADO.NET data service. I'm going to use a SQL Database at the backend, so let's go ahead and create a new one. Right click the top level item in the Solution Explorer and select "Add New Item". Find "SQL Server Database", rename it to "Customers.mdf", and click Add. You'll be prompted to put it under "App_Data", select "Yes".
Now let's add some tables to our database. Double-click Customers.mdf in the Solution Explorer, right click Tables and click "Add New Table". Go ahead and set the following table up. To make CustomerID the primary key, right click the row and select "Add Primary Key". Make sure you have matching datatypes here as well, the default of nchar(10) isn't going to work well for the CompanyName and ContactName. To make things simpler on the client-side, we're going to make CustomerID an int datatype, so that we can use it as an identity. Your table should look like this:
Now let's set CustomerID to be an identity, so that the ID increments automatically when you add to the database. In Column Properties with CustomerID selected, expand "Identity Specification", and set (Is Identity) to 1. The other defaults should be fine. If we didn't specify this, we would have to specify a unique CustomerID client-side every time we did an insert, which would quickly become difficult to manage.
After you're done with this, you can go ahead and save the table. You'll be prompted for a table name, go ahead and name the table "Contact". While we're here, let's add some data to our newly created table. Let's find it in the Server Explorer, right-click, and select "Show Table Data".
Here's the data I've entered initially and saved.
Now let's go ahead and create the actual ADO.NET Data Service and associated Entity Model. Once again, right click the top level item in the Solution Explorer and select "Add New Item". Find "ADO.NET Data Service", rename the service to "Customers.svc", and click "Add".
You're going to have two files added to your solution, Customers.svc in the root, and Customers.cs under the App_Code directory. Open up the Customers.cs class and note that there is a "TODO" comment to add your data source class name between the angled brackets. This is where we are going to plug in our entity model, which we are going to create now.
public class Customers : DataService< /* TODO: put your data source class name here */ >
Let's go ahead and add our ADO.NET Entity Model, and call it CustomersModel.edmx:
You'll be prompted to put the edmx under App_Code, select "Yes". A wizard should now pop up for you to generate your model that should look something like this:
Click "Next" with "Generate from database" selected. That should bring you to this screen, where you should be able to accept the defaults (yours may differ) and click Next. This will add a connection string to your web.config.
You will now be prompted for which database objects you would like to include in your model. Go ahead and select the "Contact" table that we created earlier, and click Finish.
This step may take a while on your machine as the model is generated. When done, you'll be shown a view of the edmx file. If you were paying close attention, notice that we left our model namespace as "CustomersModel", and our entity connection setting name is CustomersEntities. We'll need these shortly when we hook up our ADO.NET Data Service to the Entity Model.
Let's go back to our Customers.cs class, and change the first line to this:
public class Customers : DataService<CustomersModel.CustomersEntities>
so now our ADO.NET Data Service is linked to the Entity Model. We also want to go ahead and set the correct permissions so that we can access our database client-side. Remove the large "TODO" comment from the InitializeService API and replace it with the following:
config.SetEntitySetAccessRule("*", EntitySetRights.All);
config.SetServiceOperationAccessRule("*", ServiceOperationRights.All);
Let's save the Customers.cs file with these changes, and get back to our main Default.aspx page.
Website Development
Now let's write some JavaScript to actually hook up our data service. I'm going to add a JavaScript block to the <head> section of the website, right after the <title> and before the <style>. Let's start by hooking into pageLoad.
<script type="text/javascript">
function pageLoad() {
setupDataService();
}
var dataService;
function setupDataService() {
dataService = new Sys.Data.AdoNetServiceProxy("Customers.svc");
dataService.set_timeout(60000);
dataService.set_defaultFailedCallback(onFailure);
dataService.set_defaultSucceededCallback(onSuccess);
}
</script>
As you can see, on pageLoad I'm going to set up my data service by creating a new instance of AdoNetServiceProxy, which is included in MicrosoftAjaxAdoNet.js. I'm going to point it to my Customers.svc service, and set some initial parameters, like the timeout (60 seconds here), and the default failed and default succeeded callbacks. Before writing the failed and succeeded handlers though, I'm going to write some markup so that our handlers actually make sense. In the main <div> of the page, I've added the following markup:
<div>
Error Status: <input type="text" id="errorStatus" />
<fieldset style="float:left">
<legend>Master View</legend>
<div id="myDataView" sys:attach="dv" dv:itemtemplate="masterTemplate"
dv:itemplaceholder="masterPh" dv:selecteditemclass="selectedItem">
<div id="masterPh">
No data returned yet, please stand by...
</div>
</div>
<div id="masterTemplate" class="sys-template">
<a sys:id="{{$id('cust')}}" sys:command="select">{{ContactName}}</a>
<br />
</div>
</fieldset>
</div>
So here, I've added an input box to use as a error status box (as opposed to showing an alert every time you hit an error), and a fieldset which includes a basic master view. Note that the "myDataView" div attaches a dataview, and declaratively refernces an item template and an item placeholder. The item placeholder is placed inside of the dataview ("masterPh"), whereas the item template ("masterTemplate") is put outside of the dataview and has the sys-template class applied to it. Inside the item template, you will notice that I have a link that is generating a unique id and is binding to the ContactName value which will be retrieved from the ADO.NET Data Source (we will set the data imperatively through JavaScript later on). The interesting thing about this link is that it uses the sys:command syntax to map a select command to each generated link.
This concept of a selected item works with the DataView to set a selectedData and selectedIndex property on the DataView. Also note that there is a selectedItemClass property, which controls the class used for selected items. We'll visit this in detail soon. Since these properties will be set on the master, we can implement a detail view by simply binding to the selectedData property of the Master View, like so (add this fieldset under the first fieldset):
<fieldset>
<legend>Detail View</legend>
<div id="myDetailView" sys:attach="dv"
dv:data="{binding selectedData, source={{$find('myDataView')}}}"
dv:itemtemplate="detailTemplate" dv:itemplaceholder="detailPh">
<div id="detailPh">
No selected data
</div>
</div>
<div id="detailTemplate" class="sys-template">
Customer ID: <span id="customerIdInputDetail">{{CustomerID}}</span>
<br />
Company Name: <input type="text" id="companyNameInputDetail"
value="{{CompanyName}}" /><br />
Contact Name:<input type="text" id="contactNameInputDetail"
value="{{ContactName}}" /><br />
<input type="button" id="removeButton" onclick="removeCustomer()"
value="Remove" />
<input type="button" id="updateButton" onclick="updateCustomer()"
value="Update" /><br />
</div>
</fieldset>
As you can see, the detail view simply binds to the selectedData property of the master view. The item template for the detail view includes a display of the CustomerID, and input boxes with the Company Name and Contact Name, for both display and update purposes. You can see that I've also added some placeholder buttons for calling remove and update JavaScript functions.
Remember when we created the master view, that we had a selectedItemClass property that pointed to "selectedItem". Let's go ahead and define that class right now. This is the class that will be automatically applied to the selected item. Feel free to use your discretion in defining this class. Mine looks like this:
<style type="text/css">
@import url(Default.css);
.sys-template { display:none }
body {
width: 350px;
margin: 0px auto;
}
.selectedItem {
text-decoration: none;
color: White;
background-color: Black;
}
.selectedItem:hover {
text-decoration: none;
}
.selectedItem:visited {
color: White;
}
.selectedItem:active {
color: White;
}
</style>
So now let's go back to JavaScript and define our onSuccess and onFailure handlers for calls to the ADO.NET data service. I have the following handlers defined:
function onSuccess(result, userContext, operation) {
var dataView = $find("myDataView");
dataView.set_data(result);
}
function onFailure(result, userContext, operation) {
$get("errorStatus").value = result.get_message() + "\r\tStatus Code: "
+ result.get_statusCode() + "\r\tTimed Out: " + result.get_timedOut();
}
As you can see, the success handler sets the data of the master DataView to the result, and the failure handler takes the error message, status code, and timeout flag and puts it into the "errorStatus" input box we defined in markup. So now we're ready to query the service. I've made the following changes to enable this:
function pageLoad() {
setupDataService();
queryService();
}
function queryService() {
dataService.query("Contact");
}
So by default, my call to query uses the onSuccess and onFailure handlers I defined earlier. Congratulations for making it this far; we're ready to look at a preview of the website. Right click on Default.aspx and select "View In Browser" to launch the page in your default browser. If everything has gone well, you should see the placeholders for the master and detail views, and then the data you entered earlier show up. If you used my stylesheets, this should look something like this:
As you can see, by default, the first item is selected in the master view fieldset. Notice that the selectedItemClass we specified earlier is applied to the first item. Now, because the detail view is bound to the selectedData property of the master view, we're able to display different data client side just by clicking on the master links, like so:
So now we have a read-only implementation calling the ADO.NET Data Service from JavaScript. Let's add the ability to insert. First let's put another fieldset in our page, containing some markup that will make it easy for the user to insert contacts. I've chosen to put it at the top under the error status box, and I've also added a button that calls a JavaScript function that will allow me to clear any error messages that we get on insert when we're playing around with it later on.
Error Status: <input type="text" id="errorStatus" />
<input type="button" id="clearErrorButton" onclick="clearError()"
value="Clear Error" />
<fieldset>
<legend>Contact Entry (Insert) </legend>
Company Name: <input type="text" id="companyNameInput" /><br />
Contact Name:<input type="text" id="contactNameInput" /><br />
<input type="button" id="insertButton" onclick="insertCustomer()"
value="Insert" />
</fieldset>
So now our website should look something like this:
Now let's write the back-end JavaScript code for clearError() and insertCustomer(). The first function is easy, all it does is grab the textbox using $get and set the value to "", like so:
function clearError() {
$get("errorStatus").value = "";
}
The insert operation is a little more interesting. By default, the insert operation will return the item that has been inserted with additional fields and metadata populated (in this case, the CustomerID). However, in our case, we just want the Master View to update with the new list of items. So we'll make our success handler point to the query call, so once the insert is successful, the data is re-queried and the Master View is set (in the onSuccess handler of the queryService function). So the code looks like this:
function insertCustomer() {
inCompany = $get("companyNameInput").value;
inContact = $get("contactNameInput").value;
dataService.insert({ CompanyName: inCompany, ContactName: inContact },
"Contact", queryService);
}
As you can see, we're grabbing the values from the input textboxes and putting them in a dictionary as the data item to use in the call to insert. We're also specifying that we want to insert into the Contact table, and that we want to call queryService on success (since we didn't specify a failure handler, the default one will be called). Let's run our website now, and insert another contact:
So now that we have a way to insert new Contacts, let's implement the remove and update operations. The removeCustomer() function grabs the Customer ID from the selected item in the detail view, does a query for that item, and when the query succeeds, removes the item and does another query to update the Master View. It sounds pretty complex, but the code is actually quite simple:
function removeCustomer() {
custID = $get("customerIdInputDetail").innerHTML;
dataService.query("Contact?$filter=CustomerID eq " + custID, foundItem);
function foundItem(result, userContext, operation) {
if (result.length > 0) {
dataService.remove(result[0], queryService);
}
}
}
Now let's look at updateCustomer(), where we are going to do a very similar thing: query for the item we are interested in, update it with new data customer entry if it's found, and do a final query to update the Master View.
function updateCustomer() {
custID = $get("customerIdInputDetail").innerHTML;
dataService.query("Contact?$filter=CustomerID eq " + custID, foundItem);
function foundItem(result, userContext, operation) {
if (result.length > 0) {
result[0].CompanyName = $get("companyNameInputDetail").value;
result[0].ContactName = $get("contactNameInputDetail").value;
dataService.update(result[0], queryService);
}
}
}
Congratulations! At this point you should be able to play around with your site and insert, remove, and update contacts through the UI.
Bonus: Selected Index Manipulation
As you play around with the UI, you may notice the fact that the current implementation always defaults back to the first item selected. This creates an abrupt user experience that makes the queries to the data service obvious, and is not always desirable. For example, if I update a contact, I may want to keep it selected. Similarly, if I remove a contact, I may want the next contact above it to be selected. All this is possible by programmatically manipulating the selectedIndex property of the Master View. As an example, I will implement the functionality where I keep the current item selected after I update a contact.
To enable this, I can modify updateCustomer(), where I will redirect the success handler of the update call to point to my own function, queryServiceUpdate(), instead of the generic queryService() function. The queryServiceUpdate() function extracts the current index from the Master View and passes it as the userContext to a direct call to query, with its own success handler, onSuccessUpdate(). The onSuccessUpdate handler does the same thing as onSuccess, but it extracts the userContext, and uses that to set the selectedIndex on the Master View. Here's the updated function:
function updateCustomer() {
custID = $get("customerIdInputDetail").innerHTML;
dataService.query("Contact?$filter=CustomerID eq " + custID, foundItem);
function foundItem(result, userContext, operation) {
if (result.length > 0) {
result[0].CompanyName = $get("companyNameInputDetail").value;
result[0].ContactName = $get("contactNameInputDetail").value;
dataService.update(result[0], queryServiceUpdate);
}
}
function queryServiceUpdate() {
savedIndex = $find("myDataView").get_selectedIndex();
dataService.query("Contact", onSuccessUpdate, onFailure, savedIndex);
}
function onSuccessUpdate(result, userContext, operation) {
var dataView = $find("myDataView");
dataView.set_data(result);
dataView.set_selectedIndex(userContext);
}
}
Now load up the website again and notice that the update experience is much cleaner. Similar logic can be added to enable manipulation of the selectedIndex property when contacts are removed, and the ability to pass information through the userContext mechanism allows for many other scenarios in general.
Conclusion and Housekeeping
I hope you've enjoyed working through this demo and that it has given you an idea of what is possible with our new client-side ADO.NET AJAX Library. I've attached my entire project as a zip. Comments and suggestions are welcome :).
More Posts