Archives

Archives / 2005 / September
  • 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.

  • Excellent article on the features of the future VB 9

    I just read this article on the features of the future VB 9 language. The features I like the most are Linq (for both SQL and XML) and extensions. I know extensions are going to be criticized by OOP fundamentalists, but it is oh so useful and will solve so many problems purists should really just relax and enjoy. Actually, if you take a close look to Linq, you may notice that it is made possible by extensions: the where, order by and other operators are really just extensions on enumerable types from what I understand. This, with some magic reflection and database interfacing, gives you the ability to query any object graph.

  • Atlas revealed at PDC

    Atlas was revealed this morning at the PDC keynote. It was a very fast-paced demonstration by Scott Guthrie which started with the client-side querying of an Indigo service (which itself was exposing data extracted by C# 3.0's very cool query language integration) and ended with an impressive application that displays results in a templated ListView from which you can drag & drop items into a DetailsView (it's worth noting that Atlas drags and drops data and not just HTML) and then displays data on a Virtual Earth powered satellite picture of the area, complete with pinpoints, panning and zooming. And here's the best part: all that runs client-side, and it truly is cross-browser...

  • A simple ASP.NET photo album

    In June, Dmitry posted a very simple photo album on his blog. I immediately liked it because it's both easy to set-up (just drop the ashx file into your web site's photo directory) and to manage (no need to upload images one by one using a clumsy web upload field, just upload new photos using ftp or the file system). It's also nice to browse. Sure, there are no fancy ratings or comments features, you can't add titles or descriptions, but frankly, I didn't need all these features.