How to write a read/write master details page with Atlas?
The first sample is going to be a read/write master-details scenario. I'm going to use only client controls in this sample, so that you can get a good understanding of what's really going on. Maybe later I'll post a version that uses server controls which may be a little shorter but will otherwise be very similar.
I'll assume here you have a web-site that's already set-up with the Atlas binaries and script libraries. If that's not the case, please download the bits from http://atlas.asp.net.
The first thing we need to do is write some HTML. This is usually done by a web designer. The code we're going to write in this step is XHTML and CSS, so your designer does not need to know about Atlas or ASP.NET to write it. It means that it can be prepared in any web-design tool, be it DreamWeaver, FrontPage, Visual Studio or Notepad. It also means that you have complete freedom over the rendering of your page, the controls won't decide their layout for you, you decide.
We need a div where the master and details views are going to be rendered, we need a few buttons, and we need a template for each view. Here's the HTML a web designer may hand you over:
<div id="detailsView"></div>
<input type="button" id="previousButton" value="<" title="Go to previous row" />
<span id="rowIndexLabel"></span>
<input id="nextButton" type="button" value=">" title="Go to next row" />
|
<input type="button" id="addButton" value="*" title="Create a new row" />
<input type="button" id="delButton" value="X" title="Delete the current row" />
|
<input type="button" id="saveButton" value="Save" title="Save all pending changes" />
<input type="button" id="refreshButton" value="Refresh" title="Discard pending changes and get the latest data from the server" />
<br /><br />
<div id="masterView"></div>
<div id="masterTemplate">
<fieldset id="masterItemTemplate"><legend>Row <span id="masterIndex"></span>:</legend>
<b><span id="masterName"></span></b><br />
<span id="masterDescription"></span><br />
</fieldset><br/>
</div>
<div id="detailsTemplate">
Name: <input id="nameField" size="30" /><br />
Description:<br />
<textarea id="descriptionField" rows="4" cols="40"></textarea><br />
</div>
I'm not setting any styles here, but just know that there's nothing special about it, just use CSS classes as if you were writing plain old XHTML. Here's the same code as it renders in the browser unmodified (I just copied and pasted the code into the page to get live rendering):
Name:
Description:
Description:
We're going to do only one change to this code, which is to enclose the templates in a hidden div so that they don't appear on the page when we first load it. That's just a <div style="visibility:hidden;display:none"> around the two templates.
Now that's done, we're going to write the Atlas code that will bring this HTML to life by giving it behavior. For the Atlas code to work, you'll need the following script references on top of your page (inside the head is a good place):
<atlas:Script runat="server" Path="~/ScriptLibrary/AtlasCompat.js" Browser="Mozilla" />
<atlas:Script runat="server" Path="~/ScriptLibrary/AtlasCompat.js" Browser="Firefox" />
<atlas:Script runat="server" Path="~/ScriptLibrary/AtlasCompat.js" Browser="AppleMAC-Safari" />
<atlas:Script runat="server" Path="~/ScriptLibrary/AtlasCore.js" />
<atlas:Script runat="server" Path="~/ScriptLibrary/AtlasCompat2.js" Browser="AppleMAC-Safari" />
This will eventually become lighter when the ScriptManager server control will be able to output that for you.
The Atlas markup itself will live in a block such as this one, which you can place anywhere on the page (some people will like it at the bottom of the page, some will put it in the head, and there is even a server-side build provider that will enable you to put it into a completely separated .script file):
<script type="text/xml-script">
<page xmlns:script="http://schemas.microsoft.com/xml-script/2005">
<references>
<add src="../ScriptLibrary/AtlasUI.js" />
<add src="../ScriptLibrary/AtlasControls.js" />
</references>
<components>
</components>
</page>
</script>
First thing to do is to bring some data from the server. We're going to use a client-side data source that will take its data from a data service. A data service is a special kind of web service that's specialized in CRUD (Create Retrieve Update Delete) data access. It's very easy to write, you just need the four CRUD methods in a pretty free form. The parameters can be one complex type or several parameters for each of the complex type's properties, it doesn't matter as the base class should be able to figure it out using reflection. You just need to mark the methods with data object attributes:
public class SampleDataService : DataService {
static List<SampleRow> _data;
static int _nextId;
static object _dataLock = new object();
private static List<SampleRow> Data {
get {
if (_data == null) {
lock (_dataLock) {
if (_data == null) {
_data = new List<SampleRow>();
_data.Add(new SampleRow(0, "ListView", "A control to display data using templates."));
_data.Add(new SampleRow(1, "Window", "A control to display dialogs."));
_data.Add(new SampleRow(2, "Weather", "A control to display local weather."));
_nextId = 3;
}
}
}
return _data;
}
}
[DataObjectMethod(DataObjectMethodType.Delete)]
public void DeleteRow(int id) {
foreach (SampleRow row in _data) {
if (row.Id == id) {
lock (_dataLock) {
_data.Remove(row);
}
break;
}
}
}
[DataObjectMethod(DataObjectMethodType.Select)]
public SampleRow[] SelectRows() {
return SampleDataService.Data.ToArray();
}
[DataObjectMethod(DataObjectMethodType.Insert)]
public SampleRow InsertRow(string name, string description) {
SampleRow newRow;
lock (_dataLock) {
newRow = new SampleRow(_nextId++, name, description);
_data.Add(newRow);
}
return newRow;
}
[DataObjectMethod(DataObjectMethodType.Update)]
public void UpdateRow(SampleRow updateRow) {
foreach (SampleRow row in _data) {
if (row.Id == updateRow.Id) {
row.Name =updateRow.Name;
row.Description = updateRow.Description;
break;
}
}
}
}
That code should be placed in an .asmx file on the server, for example DataService.asmx. Here, we're using static data to make the sample easy to set up and because database access is not the focus of this sample. Of course, in a real application, the data would come from a real database. The data that the service handles also needs to be marked with a few attributes:
public class SampleRow {
private string _name;
private string _description;
private int _id;
[DataObjectField(true, true)]
public int Id {
get { return _id; }
set { _id = value; }
}
[DataObjectField(false)]
[DefaultValue("New row")]
public string Name {
get { return _name; }
set { _name = value; }
}
[DataObjectField(false)]
[DefaultValue("")]
public string Description {
get { return _description; }
set { _description = value; }
}
public SampleRow() {
_id = -1;
}
public SampleRow(int id, string name, string description) {
_id = id;
_name = name;
_description = description;
}
}
If you want, you may also implement a data service by overriding GetDataImplementation and SaveDataImplementation. It may be more complex to write, but you'll get rid of the reflection and won't need to attribute the data class.
That's it for the server code we're going to write. Now let's write the client-side data access. For that, let's just add the following line between <components> and </components>:
<dataSource id="dataSource" serviceURL="DataService.asmx"/>
That's it, the dataSource just needs the address of the data service. It is a very important component because in Atlas, UI components will not communicate directly with the server. They will communicate with data sources that will centralize the changes and send them back to the server as needed and as a bulk update.
Now we need to connect a ListView (which will be our master control) and an ItemView (which will be our details view) to the data on the one hand and to the HTML markup on the other hand:
<listView id="masterRepeater" targetElement="masterView" itemTemplateParentElementId="masterTemplate">
<bindings>
<binding dataContext="dataSource" dataPath="data" property="data"/>
</bindings>
<layoutTemplate>
<template layoutElement="masterTemplate"/>
</layoutTemplate>
<itemTemplate>
<template layoutElement="masterItemTemplate">
<label targetElement="masterIndex">
<bindings>
<binding dataPath="_index" transform="Add" property="text"/>
</bindings>
</label>
<label targetElement="masterName">
<bindings>
<binding dataPath="Name" property="text"/>
</bindings>
</label>
<label targetElement="masterDescription">
<bindings>
<binding dataPath="Description" property="text"/>
</bindings>
</label>
</template>
</itemTemplate>
</listView>
A few things to explain here. First, the listView control points to the target element where the rendering will take place, and to the element in the template that will contain the item template. The template for the ListView is semantically complete, meaning that there is only one template that represents header, footer and item, and then the ListView only points to the different parts. That makes the whole thing a lot easier to design and makes sure the markup is valid at all times.
Then, we have a binding that ties the data property of the ListView to the data property of the DataSource. This ensures that any time the data changes, the list can rerender to accomodate the change.
Then we have the declaration of the different parts of the template. It is particularly interesting to look at the item template. Here, we're attaching Atlas controls to the HTML elements inside the template markup. Namely, we have three labels that are each attached to a span and bind their text property to a column of the current data row.
The details view follows the same pattern. It just has textboxes instead of labels and has bidirectional bindings. It also features an additional binding that will disable the UI if the data source is not ready (which happens when it is in the process of sending changes to the server or refreshing).
<itemView targetElement="detailsView" propertyChanged="onChange">
<bindings>
<binding dataContext="dataSource" dataPath="data" property="data"/>
<binding dataContext="dataSource" dataPath="isReady" property="enabled"/>
</bindings>
<itemTemplate>
<template layoutElement="detailsTemplate">
<textBox targetElement="nameField">
<bindings>
<binding dataPath="Name" property="text" direction="InOut"/>
</bindings>
</textBox>
<textBox targetElement="descriptionField">
<bindings>
<binding dataPath="Description" property="text" direction="InOut"/>
</bindings>
</textBox>
</template>
</itemTemplate>
</itemView>
The last thing we need to do is to wire up the buttons to make the UI fully interactive:
<button targetElement="previousButton">
<bindings>
<binding dataContext="detailsView" dataPath="canMovePrevious" property="enabled"/>
</bindings>
<click>
<invokeMethod target="detailsView" method="movePrevious" />
</click>
</button>
<label targetElement="rowIndexLabel">
<bindings>
<binding dataContext="detailsView" dataPath="dataIndex" property="text" transform="Add" />
</bindings>
</label>
<button targetElement="nextButton">
<bindings>
<binding dataContext="detailsView" dataPath="canMoveNext" property="enabled"/>
</bindings>
<click>
<invokeMethod target="detailsView" method="moveNext" />
</click>
</button>
<button targetElement="addButton">
<bindings>
<binding dataContext="dataSource" dataPath="isReady" property="enabled"/>
</bindings>
<click>
<invokeMethod target="detailsView" method="addItem" />
</click>
</button>
<button targetElement="delButton">
<bindings>
<binding dataContext="dataSource" dataPath="isReady" property="enabled"/>
</bindings>
<click>
<invokeMethod target="detailsView" method="deleteCurrentItem" />
</click>
</button>
<button targetElement="saveButton">
<bindings>
<binding dataContext="dataSource" dataPath="isDirtyAndReady" property="enabled"/>
</bindings>
<click>
<invokeMethod target="dataSource" method="update" />
</click>
</button>
<button targetElement="refreshButton">
<bindings>
<binding dataContext="dataSource" dataPath="isReady" property="enabled"/>
</bindings>
<click>
<invokeMethod target="dataSource" method="select" />
</click>
</button>
On each button, we have a binding that will disable it as appropriate, and there is a click handler that will invoke the relevant method. For the previous, next, add and delete buttons, it's just a matter of calling the right method on the details view, and for the save and refresh buttons, we're just calling update or select on the data source itself.
Thanks to bindings and change notifications, we are actually done, and all changes to the data will be centralized on the data source which is the only thing that communicates with the server. Everything propagates using these simple declarative bindings and we got to make a complete master/details page without writing a single line of client script.