Building a Silverlight 3 based RIA Image Management System - 2
 
Published: 01 Sep 2009
Unedited - Community Contributed
Abstract
In the first part of this series, we examined the general design of the RIA image management system, discussed the database design, and finally started the application development from the homepage module. In this second part, we will continue to develop another module - the photo category management module, which represents one of the typical components in a Silverlight-based data-centric RIA application.
by Xianzhong Zhu
Feedback
Average Rating: This article has not yet been rated.
Views (Total / Last 10 Days): 26638/ 61

Introduction

In the first part of this series, we first examined the general design of the RIA image management system, then discussed the database design, and finally started up the application development from the homepage module. In this second installment, we will continue to develop another module—the photo category management module, which represents one of the most typical components in a Silverlight based data-centric RIA application.

Photo Category Management Module

Photo category management is one of the fundamental modules to implement in this RIA image system, which is implemented in the Silverlight control file named PhotoCatalog.xaml under the XAMLs sub-folder. In this module, our goal is provide an intuitive interface to achieve the typical CRUD operations of image category data, as well as browsing all image category data and conditional query.

First, let's familiarize ourselves with the user interface design.

Interface Design

Figure 1 indicates the design time snapshot of the image category management in Microsoft Expression Blend 3.0. Obviously, this is a simple and typical layout of user interface aimed to data management. On the left side of the interface you can perform the task of querying, browsing, adding and deleting operations; on the right is the DataGrid control with which to display associated image category data. Because the DataGrid data control support in-place editing, as soon as you finish up with your modification, you can click the "Update" button to persist the new data to the server side database.

Finally, all the CRUD operations will trigger a corresponding hint from the status message line at the farmost bottom.

Figure 1-- the design time snapshot of the image category management module

Note that when adding a new image category, we use two RadioButton controls to represent the two states, namely, public and private, of the image category. However, as the case in this article still remains in the demo level, therefore, we have not make good use of the status (Status) attribute of the image category.

Another thing worth noticing is that in Silverlight case, for the operation of the radio button is not as intuitive as in ASP.NET applications, as will be explained later on.

Here's the relevant XAML code:

Listing 1-- the radio button related XAML code

<StackPanel Orientation="Horizontal" Height="19" HorizontalAlignment="Center" 
  Margin="-1,0,0,1" VerticalAlignment="Center" Width="116">
    <RadioButton x:Name="public" Margin="5,0,0,0"
        GroupName="groupStatus" Content="Public" FontSize="14" 
        Checked="HandleCheck"/>
    <RadioButton x:Name="status" Margin="5,0,0,0"
        GroupName="groupStatus" Content="Private" FontSize="14" 
        Checked="HandleCheck"/>
</StackPanel>

It's easy to see that the two RadioButton controls are linked together (to form a group) through a common GroupName property. Clicking either of the two radio buttons will trigger the Checked event so that the event handler HandleCheck is executed. For specific programming logic with this, you will find the answer when later on the image category updating is discussed.

In the following section, we will start out to discuss the background code file PhotoCatalog.xaml.cs relating programming.

Because this part of programming quite resemble that done in the ASP.NET CRUD database operations, we plan to give introduction in accordance with the typical line of thought, first describing how to create ADO.NET Data Service, and then one by one delve into the corresponding CRUD programming.

Referencing ADO.NET Data Services and Initialization

As suggested previously, in order to achieve the server-side data access we must add the service reference in file PhotoCatalog.xaml.CS, and also, add the following two directives:

Listing 2-- two important namespaces reference

using System.Data.Services.Client;
using System.Collections.ObjectModel;

With the above references, we are able to access data services in the client functions, as well as provide a collection of ObservableCollection which represents a dynamic data collection that provides notifications. Related code is as follows:

Listing 3-- ADO.NET data services reference related initialization

//other directives omitted…
//newly referenced
using System.Collections.ObjectModel;// ObservableCollection related
using System.Data.Services.Client;
using ADONET=S2PhotoAlbum.PhotoServiceReference;//ADO.NET service reference
namespace S2PhotoAlbum.XAMLs
{
    public partial class PhotoCatalog : UserControl
    {
        // define the DataServiceContext proxy variable
        ADONET.AjaxAlbumDBEntities proxy;
        // a dynamic data collection that provides notifications, 
        // with which to provide data for DataGrid control
        ObservableCollection<ADONET.Category> boundData;
 
        public PhotoCatalog()
        {
            InitializeComponent();
            //define an instance of the DataServiceContext proxy
            proxy = new ADONET.AjaxAlbumDBEntities(
              new Uri("PhotoAlbumDataService.svc", UriKind.Relative));
    //specify each button related Click event handler
    ButtonQuery.Click += new RoutedEventHandler(ButtonQuery_Click);
    ButtonShowAll.Click += new RoutedEventHandler(ButtonShowAll_Click);
    ButtonUpdate.Click += new RoutedEventHandler(ButtonUpdate_Click);
    ButtonDelete.Click += new RoutedEventHandler(ButtonDelete_Click);
    ButtonInsert.Click += new RoutedEventHandler(ButtonInsert_Click);
}

In the initialization code above, variable AjaxAlbumDBEntities represents the ADO.NET data service client proxy. When the application is initialized, with the URI defined in the host Web site the Silverlight projects create the service instance of this agent.

Also worth researching into is the generic collection ObservableCollection variable boundData. The ObservableCollection class implements the InotifyCollectionChanged and InotifyPropertyChanged interfaces, so it features special notification functionality. As long as the collection of data change (insert new elements, change, or delete elements) a relevant notification will be sent to the DataGrid control which uses the collection as a data source, so that the binding contents inside control DataGrid are updated. ObservableCollection is a generic collection involved in Silverlight programming, which one of the important data structures. For more detailed information on this collection, please refer to MSDN.

Subsequent lines are responsible to attach the several buttons' Click event to their respective event handlers.

Next, we are going to discuss the image category data query and browsing skills.

Browsing and Querying Photo Categories

First of all, when the user clicks on the "Show All" button, this will lead to the following function invocation:

Listing 4

void ButtonShowAll_Click(object sender, RoutedEventArgs e)
{
    LoadAllData();//shows all image category data
}

Above code contains only a statement that calls a function LoadAllData. Function LoadAllData is responsible for specific task of showing all the image category data. The complete function code is as follows:

Listing 5-- The complete code for function LoadAllData

private void LoadAllData()
{
    // LINQ string with which to query all image category data
    var query =
        from category in proxy.Category
        orderby category.Status
        select category;
    try
    {
        //convert the above query string into the 
        //DataServiceQuery<ADONET.Category> type
        DataServiceQuery<ADONET.Category> cats = 
          (DataServiceQuery<ADONET.Category>)query;
        //start to execute the asynchronous query
        cats.BeginExecute(new AsyncCallback(OnInitialLoadComplete), query);
        TextBlockMessage.Text = "Query in progress, please wait…";
    }
    catch (DataServiceRequestException ex)
    {
        TextBlockMessage.Text = "Exception thrown:" + ex.Response.ToString();
    }
}

The implementation of the above process can be summarized as follows:

(1) Build the LINQ query string to query the ADO.NET data services.

(2) Convert the query string into a DataServiceQuery <ADONET.Category> type.

(3) Start to execute the asynchronous query by calling method BeginExecute, in which the first parameter specifies the callback function responsible for asynchronous query, and the second one corresponds to the query string.

All in all, the above code has completed the task of constructing the query string and sending asynchronous query task. As for the specific asynchronous query result and related handling is done inside the callback function OnInitialLoadComplete. The following lists the relevant code of this function:

Listing 6-- code for function OnInitialLoadComplete

void OnInitialLoadComplete(IAsyncResult result)
{
    try
    {
        //obtain the reference to the query
        DataServiceQuery<ADONET.Category> queryResult = 
         (DataServiceQuery<ADONET.Category>)result.AsyncState;
 
        // obtain the query result
        List<ADONET.Category> cates = queryResult.EndExecute(result).ToList();
        
        //pass the query result to the dynamic collection, so as to update 
        //the contents inside control DataGrid in time
        boundData = new ObservableCollection<ADONET.Category>();
        foreach (ADONET.Category _cat in cates)
        {
            boundData.Add(_cat);
        }
        //bind the query result to control DataGrid
        DataGridCat.ItemsSource = boundData;
        TextBlockMessage.Text = " Data load succeeded! ";
    }
    catch (DataServiceRequestException ex)
    {
        TextBlockMessage.Text = " Exception thrown: " + ex.Response.ToString();
    }
}

For now, we checked out the whole code to list all image category data. As for the image category name relevant query operation, the total implementing logic is nearly the same as that done here. So, we plan not to detail into them any more.

However, there are still a few points worth emphasizing. When the query command executes, the click event handler passes the contents of the text box as the LINQ query condition, so as to obtain image category data compatible with the specified name string. Note that the LINQ statement at this time is not implemented on the client, but converted into a URI expression that represents the query condition and attached to the proxy URI. Then, with the query request on data services the statement is sent to the server-side asynchronously, and executed by server-side data services. Later, by calling the BeginExecute method, the asynchronous query operation is implemented, while, at the same time, the current application state is also indicated to the user.

Next, we will take a look at how the important DataGrid data control supports real-time editing, as well as look into the image category data updating operation.

Updating Image Category Data

Similar to Excel’s in-place cell editing support, when double-click (or click twice at intervals) in the DataGrid control cells, the cell data can be edited directly. At this point, by clicking the "Update" button the current modified data can be updated from the long-distance server side. Note that in this operation, you must select one line in the control DataGrid, and can only select one line. (I think this is a deficiency in the control DataGrid design, while for now I have not given more examination and corresponding improvement. For typical usage relating to batch update operation, you can refer to this article, where the author mentioned a manual means to work around this problem.)

The "Update" button click event will trigger the implementation of the following function:

Listing 7-- The "Update" button click event handler

void ButtonUpdate_Click(object sender, RoutedEventArgs e)
{
    //you can only select one line
    if (DataGridCat.SelectedItem == null ||
        DataGridCat.SelectedItems.Count > 1)
    {
        TextBlockMessage.Text = "For now only one line can be updated!";
        return;
    }
    try
    {
        //obtain the instance of Category through the selected item in the grid
        ADONET.Category selectedCat = (ADONET.Category)DataGridCat.SelectedItem;
        // start to update data specified in the DataServiceContext proxy
        proxy.UpdateObject(selectedCat);
        //Persist what is modified (only one record for now) by the user 
        //to the server side in the asynchronous way
        proxy.BeginSaveChanges(OnUpdateCompleted, selectedCat);
        TextBlockMessage.Text = " Saving in progress... ";
    }
    catch (DataServiceRequestException ex)
    {
        TextBlockMessage.Text = " Exception thrown: " + ex.Response.ToString();
    }
}
//Update is Completed
void OnUpdateCompleted(IAsyncResult result)
{
    try
    {
        // end the asynchronous processing
        proxy.EndSaveChanges(result);
        TextBlockMessage.Text = " Update succeeded! ";
    }
    catch (DataServiceRequestException ex)
    {
        TextBlockMessage.Text = " Exception thrown: " + ex.Response.ToString();
    }
}

In the above code, the key lies in the following lines:

Listing 8

// update data specified in the DataServiceContext proxy
proxy.UpdateObject(selectedCat);
// Persist the modification to the server side in the asynchronous way
proxy.BeginSaveChanges(OnUpdateCompleted, selectedCat);

That is, herein, the client agent's method UpdateObject what (namely, the data selected through the DataGrid control) specified in the client proxy is updated in the client-side memory, and then by calling the client proxy's method BeginSaveChanges, real changes will be made to the server side.

Besides the above update operation, there is still an interesting episode required to be supplemented. Let's me detail into it.

To achieve the result of showing a ComboBox control within the Silverlight DataGrid control, we have to use a little sleight. In my experience, this is nearly a MUST HAVE under most situations. Take into consideration the case of ASP.NET. If you want to embed ComboBox like complex controls inside the GridView control to facilitate the user operation, you have to find ways of your own, for this is generally not supported. In this case, things are quite similar to above.

As is known, Silverlight DataGrid control not only supports the binding grammar, including three modes, namely, OneTime, OneWay, TwoWay, but also supports template binding. So, we can customize the appearance of an existing DataGrid control using template binding.

Here, again, we list the DataGrid control related markup code:

Listing 9

<Grid.Resources>
    <src:StatusProvider x:Key="statusProvider"/>
</Grid.Resources>
    <!--omittiing other details-->
<data:DataGrid x:Name="DataGridCat" 
    <!--omittiing other details-->
    <data:DataGrid.Columns>
        <data:DataGridTextColumn Header="Category" Binding="{Binding Path=Name}" 
            CanUserResize="True" CanUserSort="True" FontSize="14" 
            Foreground="#FF0000FF" />
        <data:DataGridTemplateColumn Header="Status" Width="150">
            <data:DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Path=Status}" />
                </DataTemplate>
            </data:DataGridTemplateColumn.CellTemplate>
            <data:DataGridTemplateColumn.CellEditingTemplate>
                <DataTemplate>
                    <ComboBox x:Name="comboStatus" 
        ItemsSource="{Binding StatusList, Source={StaticResource statusProvider}}"
                    SelectedItem="{Binding Path=Status, Mode=TwoWay}" 
                    src:ComboBoxService.ForceOpen="true">
                    </ComboBox>  
                </DataTemplate>
            </data:DataGridTemplateColumn.CellEditingTemplate>
        </data:DataGridTemplateColumn>
    </data:DataGrid.Columns>
</data:DataGrid>

As you've seen, the important part is pointed out in bold. Here, we embed a ComboBox control inside the DataGrid control. To the Binding attribute of the ItemsSource property of control ComboBox, we specify a static string list StatusList, and another static resource named statusProvider to the attribute Source.

Here lists the related source code:

Listing 10

public class StatusProvider
{
    public List<string> StatusList
    {
        get
        {
            return new List<string> { "Public""Private"};
        }
    }
}

Quite a short custom class! But it does help us much.

In this next section, we will turn to discuss adding new image category associated programming.

Inserting Image Category Data

The adding new image category module is the simplest one in the image category data related CRUD operations, for we nearly care for nothing that may be associated with the newly-added data. Below gives the Click event handler corresponding to the "Add" button:

Listing 11--the inserting operation related programming

// Add new image category
void ButtonInsert_Click(object sender, RoutedEventArgs e)
{
    //Create new instance of class Category with data provided by the user
    ADONET.Category cat = new ADONET.Category();
    cat.Name = TextBoxName.Text;
    cat.Status = sCheckedStatusName;
    try
    {
        //add the new Category instance into the proxy (in the client side memory)
        proxy.AddToCategory(cat);
        //submit the data to the server side
        proxy.BeginSaveChanges(OnInsertCompleted, cat);
        TextBlockMessage.Text = " Adding in progress... ";
    }
    catch (DataServiceRequestException ex)
    {
        TextBlockMessage.Text = " Exception thrown: " + ex.Response.ToString();
    }
}
//add is finished
void OnInsertCompleted(IAsyncResult result)
{
    try
    {
        //end up the asynchronous handling
        proxy.EndSaveChanges(result);
        //obtain the newly-added Category object, and append it to the 
        //collection, so that the data inside control DataGrid get updated
        boundData.Add((ADONET.Category)result.AsyncState);
        TextBlockMessage.Text = " Adding Succeeded!";
    }
    catch (DataServiceRequestException ex)
    {
        TextBlockMessage.Text = " Exception thrown:" + ex.Response.ToString();
    }
}

In sum, the implementation logic of the above code is as follows:

(1) With the data provided by the state of radio buttons and input textbox, create a new instance of Category.

(2) By calling the client-side proxy's method AddToCategory, the newly-generated Category entity object is added to the client proxy related memory.

(3) Calling method BeginSaveChanges of the client-side proxy will, in the end, add the Category entity object to the server side database.

(4) In the asynchronous callback OnlnsertCompleted, we first end up the asynchronous processing by calling method EndSaveChanges of the client-side proxy.

(5) Then, get the newly-added Category object returned from the asynchronous invocation, and append it to the collection variable boundData that bears the notification ability, so that the data inside control DataGrid get updated.

In the last part, let's delve into how to delete the specified image category data and relevant puzzles to tackle.

Deleting Image Category Data

Compared with the above insert operation, to delete the specified image category data involves some complexity of programming. We should notice that the end of the deletion of the image category does not mean that the operation is over, because there is some association relationship between the two tables Category and Photo as disclosed in the first part of this series, and as that emphasized then we have to take care of the association relationship when dealing with either of two tables, or else we may break up the reference integrity which in turn may results in invalid data existing inside the database.

So, in the actual development scenario, performance requirements for the above situation may be complex, but, on the whole, no more than two cases exist: one case is after deleting the image category data, maintain the associated image file itself unchanged and only to break the relationship between the Category instance and its related Photo instances, while the second case is to not only delete the image category instance but also remove the corresponding Photo instances. And, of course, the above removing operation should take place after the relationship between the two kinds of instances get disconnected (namely, this operation must be first). To simplify the problem, only the first case is considered in this case.

Button "Delete" related click event handler is as follows.

Listing 12-- the deleting operation related code snippet

void ButtonDelete_Click(object sender, RoutedEventArgs e)
{
    //one item should be selected
    if (DataGridCat.SelectedItem != null)
    {
        try
        {
            //select the Category instance to remove
            ADONET.Category selectedAddress = 
             (ADONET.Category)DataGridCat.SelectedItem;
            //delete one record every time
            BeginUnBindDeleteCategory(selectedAddress);
            TextBlockMessage.Text = "Deleting in progress...";
        }
        catch (DataServiceRequestException ex)
        {
            TextBlockMessage.Text = "Delete exception thrown:" + 
              ex.Response.ToString();
        }
    }
}
 
void BeginUnBindDeleteCategory(ADONET.Category categoryObject)
{
    try
    {
        proxy.BeginLoadProperty(categoryObject, "Photo",  
          EndUnBindDeleteCategory, categoryObject);
    }
    catch (DataServiceRequestException ex)
    {
        TextBlockMessage.Text = "BeginUnbindDelete exception thrown:" + 
          ex.Response.ToString();
    }
}
 
void EndUnBindDeleteCategory(IAsyncResult asyncResult)
{
    //remember the category instance to delete
    ADONET.Category categoryObject = asyncResult.AsyncState as ADONET.Category;
 
    try
    {
        //end up the navigation property loading operation
        proxy.EndLoadProperty(asyncResult);
 
        //Use API DeleteLink to disconnect all of the Photo 
        //instances from current Category instance
        foreach (ADONET.Photo photo in categoryObject.Photo)
        {
            proxy.DeleteLink(categoryObject, "Photo", photo);
        }
 
        proxy.DeleteObject(categoryObject);
        proxy.BeginSaveChanges(OnSaveChangesCompleted, null);
 
        boundData.Remove(categoryObject);//update the client side
    }
    catch (DataServiceRequestException ex)
    {
        TextBlockMessage.Text = "EndUnbindDeleteCategory exception thrown:" + 
          ex.Response.ToString();
    }
}
void OnSaveChangesCompleted(IAsyncResult result)
{
    try
    {
        proxy.EndSaveChanges(result);
        TextBlockMessage.Text = "complete!";
    }
    catch (DataServiceRequestException ex)
    {
        TextBlockMessage.Text = "OnSaveChangesCompleted exception thrown: " + 
          ex.Response.ToString();
    }
}

With the several operations introduced above, the delete operation here becomes easy to understand.

As usual, let's summary the above programming logic in the following routine:

(1) Take down the Category item to be deleted from the DataGrid control.

(2) Call method BeginUnBindDeleteCategory to undertake the specific deletion.

(3) In the BeginUnBindDeleteCategory method, according to the selected image category passed into this method call method BeginLoadProperty to load the associated navigation properties.

(4) Inside the asynchronous callback function EndUnBindDeleteCategory that corresponds to the navigation property load operation, first remember the image category to be delted. Then by calling the client proxy's method EndLoadProperty, end up the operation to load the navigation attributes.

(5) In the foreach statement, by calling the API DeleteLink, unbind all the Photo instances to the current Category instance.

(6) At this point, it's OK to safely delete the initially selected image category data.

(7) By calling the BeginSaveChanges method of the client agent, finally delete the data from the server side.

(8) By removing the Category instance data from the collection boundData, the DataGrid control related data binding has been updated.

 

Downloads

Summary

In this second part, we have designed a typical module in nearly creating any Silverlight based line-of-business applications. In other word, we have discussed the typical master-detail operations and the related items using the popular ADO.NET Data Services technology. In handling such kind of business, we should take a serious and earnest attitude towards the possible relations between the entity objects; observe as far as possible the logical rules in existence rather than violate them. In the subsequent third part we will continue to explore another important part of the system-- the image file upload and download module.



User Comments

Title: spelling mistake   
Name: abc
Date: 2009-10-30 6:33:06 AM
Comment:
spelling of deleted is wrong here :- (4) Inside the asynchronous callback function EndUnBindDeleteCategory that corresponds to the navigation property load operation, first remember the image category to be delted.






Community Advice: ASP | SQL | XML | Regular Expressions | Windows


©Copyright 1998-2019 ASPAlliance.com  |  Page Processed at 2019-04-19 4:37:34 AM  AspAlliance Recent Articles RSS Feed
About ASPAlliance | Newsgroups | Advertise | Authors | Email Lists | Feedback | Link To Us | Privacy | Search