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.