How to write a read/write master details page with Atlas?

Atlas has been unveiled, Whidbey is in Release Candidate, so it's time to update the title of this blog and start writing some sample code for Atlas. I'll continue to write articles on ASP.NET 2.0 as ideas come, but there'll definitely be a lot of Atlas stuff on the blog from now on.
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="&lt;" title="Go to previous row" />
<span id="rowIndexLabel"></span>
<input id="nextButton" type="button" value="&gt;" 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):

| |

Row :


Name:
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(truetrue)]
    
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.

14 Comments

  • Nice, thanxs for the example, gonna look into it in a moment!

  • Excellent example.

  • I tried this but nothing happens?

  • You probably made en error somewhere, but it's difficult to guess where given how little information you're giving. You can contact me through the contact page, I'll help you to figure it out.

  • Hi, thanks for article.

    I didn't figure out one thing: how to rebind detail to take place of previous snapshot? (in my case I want to bind detail text to same ID where I render preview on pageload) It's not usual master/detail scenario, but it is usefull when you have datasource with some short text preview and full version of text. I can create same effect by show/hide javascript, but I want to load full text to client on request.

    Adam

  • Enki, I'm not sure I understand what you're asking, but I think you could bind the dataIndex to the index of the item that's chosen in the master view. For example, you could have a button in the master view's item template that triggers the evaluation of an outbound binding from &quot;_index&quot; (a built-in property of DataRow) to dataIndex on the ItemView.

  • Bertrand, thx for help, but I don't understand step before this one. When I declare ListView with:

    &lt;label targetElement=&quot;resultsItemShortText&quot;&gt;

    &lt;bindings&gt;

    &lt;binding dataPath=&quot;e_shorttext&quot; property=&quot;text&quot;/&gt;

    &lt;/bindings&gt;

    &lt;/label&gt;

    and then ItemView with:

    &lt;label targetElement=&quot;resultsItemShortText&quot;&gt;

    &lt;bindings&gt;

    &lt;binding dataPath=&quot;e_longtext&quot; property=&quot;text&quot;/&gt;

    &lt;/bindings&gt;

    &lt;/label&gt;

    only ItemView is rendered (or ListView is first and ItemView renders over it instantly perhaps). I understand why, but cant figure out how make this schema working. It will be fine to have some example for this, because in database rendering apps you mostly need to draw detail right under related master data.

  • You can't have two controls pointing to the same element.

    If you need both labels to be at the same place, you could bind their visibilities to some condition and its inverse so that they never both show at the same time.

  • Hm..okay, but nobody needs AJAX enviroment for show/hide logic. What is good in AJAX idea is asynchronous handling with data, which can result in beter user experience. I'll try javascript call to webmethod which update content of parent element thru DOM.

  • solved, don't post it on forum, thx for kicking from bad idea :]

  • Thanks so much for this. I have used it on a couple of things already! I was wondering if there is any way to set the username to the membership username property automatically? I am sure it is probably really simple, but I am so used to using code behind for everything nothing comes to mind

  • Not sure what you mean. What user name?

  • Do you have the full source code from this mater page?

  • There is no master page involved here. Do you want me to publish the full source code for the master/details page? I'll do that when I have a little more time.

Comments have been disabled for this content.