By Alessandro
Del Sole – Microsoft MVP
Download
the code
Introduction
ADO.NET Data Services
(formerly known as “Project Astoria”)
is a new data platform introduced by Microsoft .NET Framework
3.5 Service Pack 1. ADO.NET Data Services can expose data, such as ADO.NET
Entity Data Models, to the Internet or a local Intranet and provide a simple,
unified model for data exchange between a server and several clients.
ADO.NET Data Services are REST-enabled WCF services and allow querying data via HTTP
requests. With Data Services we can expose different kinds of data sources
(e.g. in-memory collections or entities from an Object Model) but the most
common scenario for data-centric applications is exposing entities from an
Entity Data Model provided by the ADO.NET
Entity Framework.
For example, we can retrieve a list of customers from a Data
Service by simply writing the following URI inside the Internet Explorer
addresses bar:
http://LocalHost/Northwind.svc/Customers
The following URI retrieves all the orders made by customer
ALFKI, sorted by the order date:
http://LocalHost/Northwind.svc/Customers(‘ALFKI’)/Orders?orderby=OrderDate
These kinds of queries are called query strings and can be also used by the clients in managed code,
as we will see in the second part of this article.
Once created and deployed, ADO.NET Data Services can be consumed
by several kinds of client applications like Windows applications (Windows
Forms, Windows Presentation Foundation or Console) or Web applications
(Silverlight, AJAX, ASP.NET Web applications). Clients can then manage data
using a specific LINQ provider, called LINQ to ADO.NET Data Services. In this
article I will show you how to create an ADO.NET Data Service and how to
consume it from a Windows Presentation Foundation client, focusing on the
execution of CRUD (Create-Retrieve-Update-Delete) operations over entities
exposed by an Entity Data Model.
I will not dive deep into the server side of ADO.NET Data
Services, so I suggest you to read this great article by Mike Flasko (called Using Microsoft ADO.NET Data
Services) before proceeding with this document. Reading this article you
will learn the basics of ADO.NET Data Services, including architecture, ways to
expose data, security issues, service operations, query interceptors and custom
exceptions.
We will expose a master-details relationship through an
ADO.NET Data Service derived from some Northwind database tables
mapped into an Entity Data Model. Then we will create a WPF application for
presenting and modifying data. You should be relatively familiar with the
fundamentals of Windows
Communication Foundation, Windows Presentation
Foundation and ADO.NET
Entity Framework technologies before reading this article.
Creating the ADO.NET Data Service
We will first create an empty solution in Microsoft Visual
Studio 2008 Service Pack 1 and then we will add two projects, a Web application
and a WPF application. Start the IDE and select the File|New|Project command. When the New Project window appears, expand the Other Project Types folder, then the Visual Studio Solutions sub-folder and select the Blank Solution template, as shown in
Figure 1.
.jpg)
Figure 1 – Selecting
the Blank Solution template
Name the new solution as ConsumingDataServicesFromWPF, as shown in Figure 1, and click OK. The first step required for
exposing an ADO.NET Data Service is creating a Web application. Select the File|Add|New Project command and in the
New Project window expand the Visual Basic folder then click the ASP.NET Web Application template, as
shown in Figure 2.
.jpg)
Figure 2 – Selecting
the ASP.NET Web Application project template
Name the project as NorthwindDataService
and click OK. After a few seconds
the new project is created. At this point we need to establish a connection to
the Northwind database. For the sake
of simplicity, in the companion
code sample I have added the Northwind.mdf database file to the App_Data folder, but you can instead
establish a connection to SQL Server via the Server Explorer window if you have attached Northwind to SQL
Server. Once we have established the data connection, the next step is adding
an Entity Data Model based on
ADO.NET Entity Framework. Right click the project in Solution Explorer and select Add|New
item from the pop-up menu. When the Add
New Item window appears, just select the ADO.NET Entity Data Model item template as shown in Figure 3.
.jpg)
Figure 3 – Adding a
new Entity Data Model
Name the Entity Data Model (EDM) Northwind.edmx and click Add.
This task will start the Entity Data
Model Wizard. In the first step of the wizard we just need to specify that
the EDM will be generated from an existing database. In the second step, we
have to choose the data connection properties. Select the active database
connection from the upper combo box and verify in the Entity connection string box that the connection string is correct.
You can accept the default name of the entity connection setting in the Web.config. Figure 4 shows the
execution of this step.
.jpg)
Figure 4 – Choosing
the data connection
Click Next to go
to the next step of the wizard, where we can specify the database objects that
we want to map into the Entity Data Model. For example, we could choose the Customers, Orders and Order Details
tables as shown in Figure 5.
.jpg)
Figure 5 – Choosing
the database objects
We can also specify the Model
Namespace but for this example we’ll just accept the default name. Now
click Finish to complete the wizard.
After a few seconds you will be able to see the Visual Studio Entity Designer
showing the new entities derived from the database tables, as shown in Figure
6.
.jpg)
Figure 6 – The Entity
Framework designer
If you are familiar with the Entity Framework designer, you
already know that the Navigation Properties
represent relationships while Scalar
Properties are a managed way to represent columns from tables and that you
can use the Properties Window to
inspect and modify the properties.
Now it’s time to add the ADO.NET Data Service to the
project. As we said at the beginning of the article, Data Services are
REST-enabled Windows Communication Foundation services. This means that without
ADO.NET Data Services we could implement a WCF service by writing code to
enable REST serialization and by defining the service contracts ourselves.
However, Visual Studio 2008 comes with an item template for ADO.NET Data
Services so we don’t need to implement a custom WCF service, instead we just
need a couple lines of code to indicate how entities in our data model should
be exposed.
Now right-click the project name in Solution Explorer and select Add|New
Item. When the Add New Item
window appears, select the ADO.NET Data
Service item template and name the new item as Northwind.svc, as shown in Figure 7.
.jpg)
Figure 7 – Adding a
new ADO.NET Data Service
At this point Visual Studio will add a new ADO.NET Data
Service to our project which is the .svc
file that you can see in Solution
Explorer. Now let’s switch to the code view by double clicking the Northwind.svc file. You will see the
Visual Basic code shown in Listing 1.
Imports System.Data.Services
Imports System.Linq
Imports System.ServiceModel.Web
Public Class Northwind
' TODO: replace [[class name]] with your data class name
Inherits DataService(Of [[class name]])
' This method is called only once to initialize service-wide policies.
Public Shared Sub InitializeService(ByVal config As IDataServiceConfiguration)
' TODO: set rules to indicate which entity sets and service operations are visible,
' updatable, etc.
' Examples:
' config.SetEntitySetAccessRule("MyEntityset", EntitySetRights.AllRead)
' config.SetServiceOperationAccessRule("MyServiceOperation", ServiceOperationRights.All)
End Sub
End Class
Listing 1
This is the basic code for an ADO.NET Data Service. The Northwind class inherits from System.Data.Services.DataService(Of
T), which is the base class for a Data Service. The DataService class exposes everything we need on the server side. T represents the data source which in
our case is the class deriving from the Entity Framework ObjectContext
class (in our case it is called NORTHWNDEntities).
So replace the [[class name]] as
shown in Listing 2.
Public Class Northwind
' TODO: replace [[class name]] with your data class name
Inherits DataService(Of NORTHWNDEntities)
Listing 2
Now it’s time to establish what data we want to expose to the
web through our Data Service and what access rules must be assigned to the
data.
Setting data access rules
In real world applications it’s not uncommon to set specific
permissions or restrictions to data access and this should be always followed
as a best practice. For example, you could grant read-only access to a
particular table and read/write access to another one. ADO.NET Data Services
allow developers to set permissions for one or more entities on the server
side, both for reading and writing data.
Setting entity access rules can be accomplished using the SetEntityAccessRule
method, which receives two arguments: the entity name and the permissions level.
In our scenario we have to set permissions for the Customers, Orders and Order_Details entities. We want only
the Orders and Customers entities to be modified and updated on the client side.
So we will replace the commented SetEntityAccessRule
method provided by Visual Studio 2008 as shown in Listing 3.
config.SetEntitySetAccessRule("Customers", EntitySetRights.All)
config.SetEntitySetAccessRule("Orders", EntitySetRights.All)
config.SetEntitySetAccessRule("Order_Details", EntitySetRights.AllRead)
Listing 3
The EntitySetRights
enumeration provides a number of permissions that you can set according to your
needs. In this case we set read-only permissions for Order_Details entity and full permissions for the Customer and Orders entities. ADO.NET Data Services also provide another way to
allow or restrict data access on the server side through what is called Service Operations.
Exposing service operations
Service operations are Windows Communication
Foundation extensions and are exposed as .NET methods for data access on the
server side. For example, you could use service operations for implementing
your custom business logic or custom validation rules. Service operations are
decorated with the WebGet or WebInvoke attributes. The first one is
used for returning data while the second one is used for create/update/delete
operations. For example, we can implement a service operation for querying and
returning the order details of a given order. The query result will be returned
as an IQueryable(Of Order_Details)
object. The code for our service operation is shown in Listing 4.
<WebGet()> Public Function GetOrderDetailsByOrderId(ByVal OrderID AsInteger) _
As IQueryable(Of Order_Details)
'A simple custom validation rule based on the ID length
If OrderID.ToString.Length = 5 Then
Dim queryResult = From orderDetail In Me.CurrentDataSource.Order_Details _
Where orderDetail.OrderID = OrderID _
Order By orderDetail.Quantity _
Select orderDetail
Return queryResult
Else
Throw New DataServiceException("The specified order ID is invalid")
End If
End Function
Listing 4
Because we are retrieving data,
our method is decorated with the WebGet
attribute. This will cause an HTTP GET
request on the server side. The method body implements a very basic custom
validation rule based on the OrderID
length. In the case the length is correct the method returns a collection of Order_Details objects sorted by orders
quantity. As you can see we are retrieving data via a LINQ query. This is
because we can use a new LINQ provider for querying data exposed by ADO.NET
Data Services and that is called (as you may imagine) LINQ to ADO.NET Data
Services which is available both on the server side and on the client side.
A very important object reference
is CurrentDataSource which
represents the ObjectContext
instance that is being used to process the web request. You probably noticed
that we didn’t need to instantiate the NORTHWINDEntities context explicitly.
This is because the ADO.NET Data Services engine instantiates the ObjectContext
for us, so CurrentDataSource
represents the context instance on the server side.
In order to expose the service
operation GetOrderDetailsById to
clients we need to set permissions the
same way we did for the entities. Setting permissions to service operations can
be accomplished using the SetServiceOperationAccessRule
method which works like SetEntityAccessRule
but is specific to service operations. Our method just returns data, so we can
set permissions as shown in Listing 5 inside the InitializeService method.
config.SetServiceOperationAccessRule("GetOrderDetailsByOrderId", ServiceOperationRights.AllRead)
Listing 5
Setting the AllRead value for the ServiceOperationRights
enumeration will allow accessing data just for reading. Service operations are
very useful methods because they can return different CLR types, such as
primitive types, IQueryable(Of TEntity)
or IEnumerable(Of TEntity). You
should also note that service operations must be implemented as public instance
methods.
At this point we are ready to
test our service from within a Web browser like Microsoft Internet Explorer.
Testing the service
We are now ready to check if our
service works properly. Press F5 and
after a few seconds you should see your Web browser showing an XML
representation of the three entities, as shown in Figure 8. Please remember to
disable the RSS view in your Web browser. In case of Microsoft Internet Explorer
7, choose the Tools|Internetoptions command and then the Content tab where you will find the Feed settings.
.jpg)
Figure 8 – The ADO.NET Data Service is properly running
As we said at the beginning of
the article, ADO.NET Data Services can be queried using query strings in HTTP
Get requests. For example, we can retrieve a one-to-many relationship obtaining
all the orders made by the CACTU
customer, sorting orders according to the ShipCity
property. We can accomplish this by writing the following URI in the Web
browser addresses bar:
http://localhost:1653/Northwind.svc/Customers('CACTU')/Orders?OrderBy=ShipCity
The result of this query is shown
in Figure 9.
.jpg)
Figure 9 – Retrieving a one-to-many relationship using a query string
The last example I want to
provide shows how to call service operations on the server side. According to
the service operation we implemented before, we could retrieve all the order
details related to the OrderID 10500.
We can do this by typing the following URI:
http://localhost:1653/Northwind.svc/GetOrderDetailsByOrderId?OrderID=10500
As you can see, the service
operation’s name is part of the URI and can receive query string arguments
according to the URI syntax for Data Services. This query string will produce
the result shown in Figure 10.
.jpg)
Figure 10 – Using a service operation on the server side
As you can see scrolling the
page, you have successfully obtained a list of all the order details for the
desired order.
Now we can be sure that our
ADO.NET Data Service is working properly. But it would surely make more sense
for us to reference the Data Service from a client application and provide a
user interface for consuming data. This is what we’re going to do in the next
section.
For further information about
query strings and the server side of ADO.NET Data Services, please read Mike Flasko’s “Using ADO.NET Data Services”
article.
Creating the WPF client
Client applications that consume ADO.NET Data Services
reference the ADO.NET Data Services Client
library, and can be both Windows applications and Web applications. In this
article we will create a Windows Presentation Foundation client. For the sake
of simplicity, let’s add the new project to our existing solution. We need a
WPF project in Visual Basic, as shown in Figure 11.
.jpg)
Figure 11 – Adding a
new WPF project to the solution
We can simply name our new project WpfClient. The first step is adding a service reference to the
ADO.NET Data Service just like we would to any other Windows Communication
Foundation service. So, right-click the project name in Solution Explorer and select Add
service reference. In the Add
service reference dialog we must provide the service URI. In this case the
service resides in the same solution and is hosted by the ASP.NET development
server so we can just click the Discover
button and Visual Studio will automatically list all the WCF services included
in the solution (of course just one in our case). Click the service name so
that the ASP.NET runtime will host the service itself then specify NorthwindServiceReference as the
namespace identifier. Figure 12 shows the result of all the above mentioned
tasks.
.jpg)
Figure 12 – Adding a
service reference to the ADO.NET Data Service
In a real world application the service will be probably
hosted by a Web site, so you will just need to change the URI pointing to the
service address (e.g.: http://www.datasite.com/MyService.svc).
After a few seconds the service reference is added (you can check this in Solution Explorer) which generates the
entities that are exposed by the Data Service. It also adds a reference to the client library,
System.Data.Services.Client, which is the client proxy used to access the data
service. The client library handles the details of
mapping LINQ statements to a URI in the data service and retrieving the
specified entities as .NET objects.
Now we’re going to build a user interface like the one shown
in Figure 13.
.jpg)
Figure 13 – The client
application’s user interface
As you can see, the application will show master-details
relationships for Customers and Orders and will allow CRUD operations
(Create/Read/Update/Delete) onto the Orders
table. Moreover, you will be able to show the order details for each order.
Figure 14 shows an example of the order details window.
.jpg)
Figure 14 – The order
details window of the client application
At last, Figure 15 shows the window we will implement for
insert operations.
.jpg)
Figure 15 – Adding a
new order via user interface
As you can see, the user interface is quite simple. Although
we decided to create a WPF application we don’t need enhanced graphical effects
or animations because in this case we want to focus on how we can consume
ADO.NET Data Services from WPF clients and on WPF data-binding techniques. We
can now begin writing code, providing useful methods for CRUD operations
against the ADO.NET Data Service using the client library.
Implementing methods for CRUD operations
It’s a good idea to create a separate class for providing
methods that will run CRUD operations so that your code can be easily reused.
This approach is also used in the code samples provided by the Microsoft.NET
Framework 3.5 Enhancements Training Kit (check it out if you didn’t before,
because it contains lots of slides, demos and labs about ADO.NET Data
Services). First let’s add a new class named Helper.vb to our client. Let’s begin by adding a couple of Imports directives (see Listing 6).
'Needed for service's objects
Imports WpfClient.NorthwindServiceReference
'Needed for handling Data Services exceptions
Imports System.Data.Services.Client
The next step is declaring the ObjectContext object that we can instantiate in the constructor, as shown in Listing 7.
Public Class Helper
Public NorthwindContext As New NORTHWNDEntities(New Uri("http://localhost:1653/Northwind.svc"))
Listing 7
When instantiating the data service client’s
DataServiceContext (in this case NORTHWNDEntities)
we need to pass an argument of type Uri
which represents the address of the ADO.NET Data Service which is the same we
specified when we added the service reference. It also is a good idea to store
the Uri in the configuration settings inside the App.config file. For this example, we need to implement a pair of
methods for retrieving customers and orders. We can easily accomplish this by
using LINQ to ADO.NET Data Services,
as shown in Listing 8.
Public Function GetCustomers() As _
IQueryable(Of Customers)
'LINQ to DataServices in action:
Dim customerQuery = From customer In Me.NorthwindContext.Customers _
Order By customer.ContactName _
Select customer
Return customerQuery
End Function
Public Function GetOrders(ByVal Customer _
As Customers) As IQueryable(Of Orders)
'Eager loading with the Expand method
Dim ordersQuery = From order In Me.NorthwindContext.Orders.Expand("Customers") _
Where order.Customers.CompanyName = Customer.CompanyName _
Order By order.OrderDate _
Select order
Return ordersQuery
End Function
Listing 8
The GetCustomers
method simply returns an IQueryable(Of
Customers) which is the result of a really simple LINQ query. The GetOrders method is a little more
interesting because it shows a particular technique called eager loading. This technique allows retrieving data of a
one-to-many relationship and the ADO.NET Entity Framework provides a method
called Expand to accomplish this.
This method is exposed from each entity set. The Expand method is invoked against the “many” part of the
relationship and receives, as an argument, the entity set that represents the
“one” part of the relationship. Orders are sorted by the OrderDate property while the filter is done by comparing the Customer.CompanyName property. This
happens because the user will be able to select the customer in the UI choosing
the company name.
Now we can write code for the CUD (Create-Update-Delete)
operations. The first two operations we want to handle are the Update and Delete. Listing 9 shows the code that I will explain below.
Public Sub DeleteOrder(ByVal Order As Orders)
Try
Me.NorthwindContext.DeleteObject(Order)
Me.NorthwindContext.SaveChanges()
Catch ex As DataServiceRequestException
Throw New DataServiceRequestException(ex.InnerException.Message)
Catch ex As ArgumentNullException
Throw New ArgumentNullException("No row was selected.")
End Try
End Sub
Public Sub UpdateOrder(ByVal Order As Orders)
Try
Me.NorthwindContext.UpdateObject(Order)
Me.NorthwindContext.SaveChanges()
Catch ex As DataServiceRequestException
Throw New DataServiceRequestException(ex.InnerException.Message)
Catch ex As ArgumentNullException
Throw New ArgumentNullException("No row was selected.")
End Try
End Sub
Listing 9
Both the UpdateOrder
and DeleteOrder methods receive an
argument of type Orders, which is
the entity to be updated or deleted. They will respectively invoke the UpdateObject and DeleteObject methods that will tell the data service client to
either mark the entity for update or delete. The SaveChanges method tells the data service client to send these
entity changes to the ADO.NET Data Service which uses the Entity Framework to
modify the underlying database. Since both methods will be invoked by the
presentation window, if the user clicks the associated buttons without
selecting any order then a null object will be passed as an argument. This is
why we are handling an ArgumentNullException.
The exception is re-thrown to the caller that will handle it in a user-friendly
way. The same technique is used regarding the DataServiceRequestException
that is thrown by the service if HTTP requests fail.
It’s worth mentioning that we also have the option of
sending changes in batch, meaning that a group of operations can be sent via
HTTP to the service. This is a good choice when we have multiple entities to be
submitted or updated in a single database transaction and it reduces the number
of requests we send to the service. So instead of calling SaveChanges after
every operation, we can call it once to send all updates, inserts and deletes
in one shot. In our case this could be accomplished by exposing a SaveChanges
method from the Helper class, passing the
System.Data.Services.Client.SaveChangesOptions.Batch argument. This is not
necessary in our example because we are not sending order details, but if we
did, then we would want to implement this additional method. Further
information about sending changes in batch can be found in the MSDN Library.
At this point we need to write code for adding a new order
to the database through the data service. Listing 10 shows the implementation
of a new method called AddNewOrder.
Public Sub AddNewOrder(ByVal Order As Orders)
Try
Me.NorthwindContext.AddToOrders(Order)
'Explicitly setting relationships
Me.NorthwindContext.SetLink(Order, "Customers", Order.Customers)
Me.NorthwindContext.SaveChanges()
Catch ex As DataServiceRequestException
Throw New DataServiceRequestException(ex.InnerException.Message)
Catch ex As ArgumentNullException
Throw New ArgumentNullException("Cannot add a null object.")
End Try
End Sub
Listing 10
In the AddNewOrder
method there is some interesting code. Notice that there is an AddTo method for each entity exposed by
the service. This was automatically generated on the NorthwindEntities DataServiceContext
when we added the ADO.NET Data Service reference to our client. The AddToOrders method will add a new Order entity to the collection. Also
notice that we must explicitly set the relationship between the newly added
order and its corresponding customer. This is accomplished by using the SetLink instance method that receives
three arguments: the new entity’s instance (in this
case a new Order), a string for the EntitySet
representing the “one” association in the one-to-many relationship, and the
.NET property that represents the “one” association in the one-to-many
relationship in which the new entity (Order)
belongs.
Note: You may be
confused because the single association is called Customers and not Customer.
This is because the names of the tables in the Northwind database are not
singular. The names of the entities and the associations can easily be changed
on the data model using the Visual Studio Entity Data Model designer and then
rebuilding and updating the service reference on the client.
At this point we can implement a method that will call the
service operation for retrieving the order details related to a particular
order. As we said before, service operations that just read and return data are
just simple HTTP GET requests to the
service. The ADO.NET Data Services framework provides an overload of the Execute(Of TElement) method for
HTTP GET requests. This method receives an argument of type Uri that is represented by a query
string containing the name of the service operation. So we could implement our
method as shown in Listing 11.
Public Function QueryOrderDetails(ByVal OrderId As Integer) As IQueryable(Of Order_Details)
Try
Return Me.NorthwindContext.Execute(Of Order_Details) _
(New Uri("GetOrderDetailsByOrderId?OrderID=" + _
OrderId.ToString, UriKind.Relative)).AsQueryable
Catch ex As Exception
Throw
End Try
End Function
End Class
Listing 11
The Execute(Of
TElement) method returns an IEnumerable(Of
T), where T is an entity. Since
a cast to IQueryable(Of T) is needed
we can use the AsQueryable extension
method for this purpose. The Uri
argument is a string that we can build combining the service operation name and
the OrderID.
Now it’s time to build the user interface.
Implementing value converters
Often users submit data as plain text, via the UI. Such text
is often a representation of a .NET type; you may think of an order date
submitted by the user and which must be first validated and then represented in
the UI in the appropriate format. When developing WPF applications, you can
implement valueconverters to control the input and output formatting. Value
converters are .NET classes which provide custom logic for binding data to the
UI and converting an Object into another .NET data type. The result of the
conversion can be bound to the UI writing a XAML markup extension. Using value
converters as opposed to simpler display formatting gives you full control over
the input values users will enter. Value converter classes implement the IValueConverter
interface, which requires two methods to be implemented: Convert
and ConvertBack.
In our case we need to convert order dates submitted as strings by the user and
bind the result of such conversion to the UI, so that dates will be recognized
as DateTime objects instead of plain text. At this point we need to add a class
to our project (Project|Add class)
and name the class DateConverter.vb.
Code for the DateConverter class is shown in Listing 12.
<ValueConversion(GetType(String), GetType(DateTime))> _
Public Class DateConverter
Implements IValueConverter
Public Function Convert(ByVal value As Object, _
ByVal targetType As System.Type, _
ByVal parameter As Object, _
ByVal culture As System.Globalization.CultureInfo) _
As Object Implements System.Windows.Data.IValueConverter.Convert
If value IsNot Nothing Then
Dim ResultDate As DateTime = CType(value, DateTime)
Return ResultDate
Else
'If the value is null, returns an empty string
Return String.Empty
End If
End Function
Public Function ConvertBack(ByVal value As Object, _
ByVal targetType As System.Type, _
ByVal parameter As Object, _
ByVal culture As System.Globalization.CultureInfo) As Object _
Implements System.Windows.Data.IValueConverter.ConvertBack
'We know we are receiving strings (via TextBox)
Dim CurrentValue As String = CStr(value)
Dim ResultDate As DateTime = Nothing
If DateTime.TryParse(CurrentValue, ResultDate) Then
Return ResultDate
Else
'A default value is returned in case of invalid date
Return DateTime.Now
End If
End Function
End Class
Listing 12
Adding a ValueConversion attribute to the value converters
definition is a best practice, because it allows specifying what data types
will be involved in the conversion. Both the Convert and ConvertBack methods
provide logic for converting strings into the appropriate DateTime
representation. Notice how the ConvertBack method will return a default value
(DateTime.Now) in case the parsing of the original string doesn’t match a valid
date representation. We will see the usage of this converter later in the UI
implementation.
Building the user interface
We’ll begin implementing the user interface starting from
the order details window which is the simplest window among the ones we saw
above. This can be useful to understand some data-binding techniques in Windows
Presentation Foundation. The first step is adding a new WPF window, so right
click the project name in Solution
Explorer and select the Add|New
window command. When the Add New
Item dialog appears, just specify a name for the new window (e.g. ViewOrderDetails.xaml) as shown in
Figure 16.
.jpg)
Figure 16 – Adding a
new WPF window to the project
Our new window will look like what we saw in Figure 14, so
we need to divide the window itself into two parts. The top portion of the
window will contain a ListView
for showing tabular data and below that there will be a button to close the
window. To accomplish this we can declare a Grid
control and split it into two rows. Listing 13 shows the beginning of the new
window’s XAML code, where you can see how the window title has been changed and
how the Grid and a Button are declared.
<Window x:Class="ViewOrderDetails"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="View order details" Height="300" Width="350">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="40"/>
</Grid.RowDefinitions>
<Button Grid.Row="1" Width="100" Height="30" Content="Close" Name="CloseButton"
Click="CloseButton_Click"/>
Listing 13
Now we have to define how data will be presented to the
user. For showing tabular data we can use the ListView control. When using the ListView we can declare a DataTemplate
for each cell, so that we can define how data must be presented (e.g. if we
have a Boolean value to be shown we
can declare a CheckBox to show it).
Let’s begin by writing the code shown in Listing 14 which declares the
appearance of the ListView (see
comments inside the code).
<!--The ItemsSource is data-bound
but suspended and will be populated at runtime-->
<ListView ItemsSource="{Binding}"
Grid.Row="0"
Name="OrdeDetailsListView"
Margin="5" >
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<!--This will allow each column
to be stretched to fit its content-->
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<!--This will ensure currency between the
selected item and the data source-->
<EventSetter Event="GotFocus"
Handler="Item_GotFocus"/>
</Style>
</ListView.ItemContainerStyle>
Listing 14
The most important thing in the above code is the ItemsSource
property assignment. Using the {Binding} markup
extension we are establishing that the property is data-bound but the data
source will be assigned at runtime via Visual Basic code. Now we can implement
a ListView custom template for
showing tabular data. This can be accomplished by assigning a GridView
item to the ListView.View
object. The GridView will contain as
many GridViewColumn
objects as many properties we want to show. For each GridViewColumn we can define a DataTemplate
object that represents the way data will be presented to the user. Everything
we discussed is shown in Listing 15.
<ListView.View>
<GridView>
<GridViewColumn Header="Product ID">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=ProductID}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Unit price">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=UnitPrice, StringFormat=c}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Quantity">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Quantity}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Discount">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Discount, StringFormat=p}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
</Grid>
</Window>
Listing 15
Notice how data-binding is established by the Binding markup extension indicating the
name of the property in which to bind. This means that regardless of the name
and type of the data source (in this case an Order_Details entity) WPF will data bind to the value of the
specified property. We can now switch to the Visual Basic code editor and
complete the code for this window. The code is shown in Listing 16 (see
comments for explanations).
Imports WpfClient.NorthwindServiceReference
Partial Public Class ViewOrderDetails
'The OrderID will be passed by the caller
'(the main window)
Public Sub New(ByVal OrderID As Integer)
InitializeComponent()
Dim helperClass As New Helper
Me.OrdeDetailsListView.ItemsSource = helperClass.QueryOrderDetails(OrderID)
End Sub
Public Sub New(ByVal Source As IQueryable(Of Order_Details))
' This call is required by the Windows Form Designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
'This will populate the ListView
Me.OrdeDetailsListView.ItemsSource = Source
End Sub
'Set correspondence between the selected item
'and the data source
Private Sub Item_GotFocus(ByVal sender As System.Object, _
ByVal e As System.Windows.RoutedEventArgs)
Dim item = CType(sender, ListViewItem)
Me.OrdeDetailsListView.SelectedItem = item.DataContext
End Sub
Private Sub CloseButton_Click(ByVal sender As System.Object, _
ByVal e As System.Windows.RoutedEventArgs)
Me.Close()
End Sub
End Class
Listing 16
Notice that we modified the constructor to receive an OrderID argument (passed by the caller)
of type Integer which we pass to the
Helper.QueryOrderDetails method.
Invoking this method will return an IQueryable(Of
Order_Details) object that will constitute the data source that will
populate the ListView. The data
source is then assigned to the OrderDetailsListView.ItemsSource
property which sets up the data binding.
The second window we need to create for the user interface
is the one for submitting new orders. For the sake of simplicity, I will show
you how to create and submit a new order setting just a few properties for each
order. You will then be able to extend the code and set the full range of
properties for each order. Please follow the same steps shown before in order
to add a new window to the project called NewOrder.xaml.
According to the appearance we saw in Figure 15, we can implement the UI in a
very simple way with the XAML code shown in Listing 17.
<Window x:Class="NewOrder"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfClient"
Title="Insert a new order" Height="300" Width="300">
<Window.Resources>
<local:DateConverter x:Key="dateConverter"/>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition/>
<RowDefinition Height="40"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="Please specify new order's properties:" />
<Grid Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Vertical">
<TextBlock Margin="2" Text="Order Date:"/>
<TextBox Margin="2" Name="OrderDateTextBox"
Text="{Binding Path=OrderDate,
Converter={StaticResource dateConverter}, ConverterParameter='d'}" />
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Vertical">
<TextBlock Margin="2" Text="Required Date:"/>
<TextBox Margin="2" Name="RequiredDateTextBox"
Text="{Binding Path=RequiredDate,
Converter={StaticResource dateConverter}, ConverterParameter='d'}"/>
</StackPanel>
<StackPanel Grid.Row="2" Orientation="Vertical">
<TextBlock Margin="2" Text="Shipped Date:"/>
<TextBox Margin="2" Name="ShippedDateTextBox"
Text="{Binding Path=ShippedDate,
Converter={StaticResource dateConverter}, ConverterParameter='d'}"/>
</StackPanel>
<StackPanel Grid.Row="3" Orientation="Vertical">
<TextBlock Margin="2" Text="Ship City:"/>
<TextBox Margin="2" Name="ShipCityTextBox"
Text="{Binding Path=ShipCity}"/>
</StackPanel>
</Grid>
<StackPanel Grid.Row="2" Orientation="Horizontal">
<Button Width="100" Height="30" Content="Submit" Margin="5"
Name="SubmitButton" Click="SubmitButton_Click" />
<Button Width="100" Height="30" Content="Cancel" Margin="5"
Name="CancelButton" Click="CancelButton_Click" />
</StackPanel>
</Grid>
</Window>
Listing 17
With the StackPanel
object we can arrange child elements into a single line both horizontally and
vertically. As you can see we have just implemented controls for receiving
information by the end user. Such information will populate the new order’s
properties.
At this point we must focus on other interesting techniques
demonstrated in the above XAML. First we imported an Xml namespace called local pointing to our assembly. This
will allow us to use classes defined in Visual Basic code also in the XAML
code. You should rebuild the project to ensure that the namespace is correctly
recognized by the IDE. Second, in the Window resources we defined a dateConverter identifier which allows
us to call in XAML code the DateConverter
class that we previously declared in Visual Basic (please remember that
identifiers in XAML are case sensitive). Also notice how the text property for
each TextBox control is set via data-binding. The Binding XAML markup extension has an attribute called Path pointing to the related property
of each Order object that must be
shown. Take a look at the usage of the value converter we implemented before;
for the three TextBoxes that receive dates a Converter attribute is specified.
This attribute needs another XAML markup extension which points to the converter
class (StaticResource) while the ConverterParameter indicates what kind
of format must be applied to the returned data (in this case ‘d’ means a short date format).
Implementing such techniques will be reflected upon properties set for each new
order in the main Window.
Now let’s switch to the Visual Basic code editor and type
the code shown in Listing 18 which will initialize the window and set event
handlers for the buttons.
Imports WpfClient.NorthwindServiceReference
Partial Public Class NewOrder
Private _order As Orders
Public Property Order() As Orders
Get
Return _order
End Get
Set(ByVal value As Orders)
_order = value
'Binds the controls to this order
Me.DataContext = _order
End Set
End Property
Private CurrentCustomer As Customers
Public Sub New(ByVal Customer As Customers)
' This call is required by the Windows Form Designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
Me.CurrentCustomer = Customer
End Sub
Private Sub SubmitButton_Click(ByVal sender As System.Object, _
ByVal e As System.Windows.RoutedEventArgs)
Me.Order.Customers = Me.CurrentCustomer
Me.DialogResult = True
Me.Close() End Sub
Private Sub CancelButton_Click(ByVal sender As System.Object, _
ByVal e As System.Windows.RoutedEventArgs)
Me.DialogResult = Nothing
Me.Close()
End Sub
Listing 18
As you can see in Listing 18, the constructor receives the Customer object that is the owner of
the new order, so that we can set a relationship between the Customer and the new order when the SubmitButton is clicked. We also
implemented an Order property that
is submitted to the underlying database which is also bound to the Customer. We
also assign a dialog result before closing the window, depending on the user’s
choice. In Windows Presentation Foundation the DialogResult is of type Nullable(Of
Boolean), so a Nothing value
means that the user canceled the operation while a True value means that the user completed the operation.
When clicking the Submit
button, the AddNewOrder of our helper class is invoked.
Obviously you can implement more complex and efficient
validation rules. Regarding this, I suggest you to read this blog
post by Beth Massi where she
discusses how you can implement custom data validation in WPF forms and this
one where she talks about WPF converters.
Now we can finally design the main window of the
application. In Solution Explorer
double click the Window1.xaml code
file to activate the WPF designer. Please refer to Figure 13 in the following
steps related to the UI implementation. First of all we can change the Window’s
Title property and declare a Grid which will contain some children
controls: a nested Grid where we
will put a description and a ComboBox
for the customer’s selection, a ListView
for presenting and manipulating data, a StackPanel
that will contain buttons for invoking CRUD
operations. Listing 19 shows how to declare the first two Grids, the description
and the ComboBox.
<Window x:Class="Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Manage your orders" Height="350" Width="650">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="40"/>
<RowDefinition/>
<RowDefinition Height="50"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Please select a customer: " />
<ComboBox Grid.Column="1" Margin="5"
DisplayMemberPath="CompanyName"
Name="CustomersCombo" />
</Grid>
Listing 19
We will populate the ItemsSource
of the ComboBox control via Visual
Basic code to the Customers entity
set. Another interesting property is DisplayMemberPath
which will show the value of the specified property name in the data bound
collection. In other words, if we assign a collection of Customers as a data
source to the ComboBox.ItemsSource
property, then we need to specify what property of the collection can be shown
by the ComboBox itself for each item in the collection. This is accomplished by
using the DisplayMemberPath
property.
Similar to what we did in the ViewOrderDetails window, we can now declare a ListView object for tabular data. In this case we will use several TextBox controls for each DataTemplate object, because Orders entities can be modified
according to the permissions we set on the server side. Listing 20 shows the
code for the ListView.
<ListView ItemsSource="{Binding}" Grid.Row="1" Name="OrdersListView" Margin="5" >
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<EventSetter Event="GotFocus" Handler="Item_GotFocus"/>
</Style>
</ListView.ItemContainerStyle>
<ListView.View>
<GridView>
<GridViewColumn Header="Order ID">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=OrderID}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Order Date">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding Path=OrderDate}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Freight">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding Path=Freight}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Required Date">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding Path=RequiredDate}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Ship Address">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding Path=ShipAddress}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Ship City">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding Path=ShipCity}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Ship Country">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding Path=ShipCountry}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Ship Region">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding Path=ShipRegion}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Ship Postal Code">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding Path=ShipPostalCode}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Ship Name">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding Path=ShipName}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Shipped Date">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding Path=ShippedDate}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
Listing 20
TextBox controls allow the so-called “two-way” data-binding.
This means that data-bound objects can display their properties as well as have
them modified via the data bound UI controls. In our example we have mapped all
the properties in the Orders entity
to ListView columns but you could
also decide to restrict the mapping. Only the OrderID ListView column is read-only (in fact we declared a TextBlock
object) because that is the primary key and is managed by the SQL Server engine
even when submitting new orders.
The last step in declaring UI objects for the main window is
adding some buttons that will be associated to some tasks. Buttons can be
nested into a StackPanel container
and a useful approach is to define a WPF style for assigning the same
properties to each button just once. Listing 21 shows the implementation of the
StackPanel and children buttons.
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="5">
<StackPanel.Resources>
<!-- A simple style for setting Buttons properties-->
<Style x:Key="ButtonStyle" TargetType="Button">
<Setter Property="Margin" Value="5"/>
<Setter Property="Width" Value="120"/>
<Setter Property="Height" Value="30"/>
</Style>
</StackPanel.Resources>
<Button Content="New order" Style="{StaticResource ButtonStyle}"
Name="NewOrderButton" Click="NewOrderButton_Click" />
<Button Content="Update order" Style="{StaticResource ButtonStyle}"
Name="UpdateOrderButton" Click="UpdateOrderButton_Click" />
<Button Content="Delete order" Style="{StaticResource ButtonStyle}"
Name="DeleteOrderButton" Click="DeleteOrderButton_Click" />
<Button Content="View order details" Style="{StaticResource ButtonStyle}"
Name="OrderDetailsButton" Click="OrderDetailsButton_Click" />
</StackPanel>
</Grid>
</Window>
Listing 21
Defining a style is really useful because we can define the
same properties for a particular type of control just once. In this case the
style defines the Width, the Height and the Margin properties for the buttons and is assigned to each button
via a markup extension called StaticResource.
At this point ensure that the UI you see in the designer is the same of the one
shown in Figure 13. Now we can switch to the Visual Basic code editor and
finalize the application. We have to consider that when the application is
running the first thing to do is to populate the ComboBox with a list of customers. Once that is completed, the
application must load the orders associated with the first customer in the
list. We also need to provide the logic for updating the list of orders when
the user selects another customer from the ComboBox. To accomplish some of
these tasks we will invoke methods provided by the Helper.vb class (see the Implementing
methods for CRUD operations section of the article). Code in Listing 22
shows how we can do this (see comments for explanations).
Imports WpfClient.NorthwindServiceReference
Imports System.Data.Services.Client
Class Window1
Private WithEvents OrderView As ListCollectionView
Private HelperClass As New Helper
Public Sub New()
' This call is required by the Windows Form Designer.
InitializeComponent()
'Populating the ComboBox
Me.LoadCustomers()
End Sub
'Populating the customers ComboBox
Private Sub LoadCustomers()
Me.CustomersCombo.ItemsSource = Me.HelperClass.GetCustomers
Me.CustomersCombo.SelectedIndex = 0
End Sub
'Populating the orders ListView
Private Sub LoadOrders()
'First I need to get the current object in the ComboBox
'then I must convert that object into a Customers instance
Dim SelectedCustomer As Customers = _
CType(CustomersCombo.SelectedItem, Customers)
Me.DataContext = Me.HelperClass.GetOrders(SelectedCustomer).ToList
End Sub
'Set currency between the selected item
'and the data source
Private Sub Item_GotFocus(ByVal sender As System.Object, _
ByVal e As System.Windows.RoutedEventArgs)
Dim item = CType(sender, ListViewItem)
Me.OrdersListView.SelectedItem = item.DataContext
End Sub
'When the user selects a customer then loads the associated orders
'into the ListView
Private Sub CustomersCombo_SelectionChanged(ByVal sender As Object, ByVal e As _
System.Windows.Controls.SelectionChangedEventArgs) _
Handles CustomersCombo.SelectionChanged
Me.LoadOrders()
End Sub
Listing 22
Note that we created just one instance of the Helper class
so that we have the flexibility of sending changes to the service in one call,
instead of sending multiple requests over the network. As mentioned before, all
we would need to do to enable that in the Helper is move the calls to
SaveChanges into one method and expose that to our form as well.
Handling the ComboBox.SelectionChanged
event will allow us to load and show the orders associated with the customer
that the user selected in the CustomersCombo
control, invoking a method called LoadOrders. Regarding this method, we have to
say that at the beginning of the code in Listing 22, we declared an object OrderView of type ListCollectionView. This is a particular .NET object which offers a
view for manipulating data. It acts over your collections exposing methods for
adding, removing and editing data and can be used in data-binding operations,
as we will see later. At the moment we just need to focus on the
Window.DataContext property assignment: a list of orders, retrieved by the
Helper.GetOrders method and then converted into a generic List(Of Orders)
collection, becomes the data source for the ListView control. There are a
couple different types of CollectionView objects that are returned depending on
the type of collection you are binding to but the ListCollectionView is used
when working with simple List(Of T) collections.
At this point we just need to write event handlers for the
buttons’ Click events. Here we will
invoke some other methods exposed by the Helper.vb
class specifically for executing CRUD operations as shown in Listing 23 (see
comments in the code for a better exposition).
Private Sub NewOrderButton_Click(ByVal sender As System.Object, _
ByVal e As System.Windows.RoutedEventArgs)
'Adding a new Order to the List
'First, adds to the instance of the ListCollectionView
'a new item of the specified type (Orders)
Dim ord As Orders = CType(Me.OrderView.AddNew(), Orders)
'then moves the ListView selection to point to the new item
Me.OrdersListView.ScrollIntoView(ord)
'The NewOrder dialog's constructor receives a Customer as an argument.
'In this case we want to associate the new order to the current Customer
'so we retrieve the instance and pass it to the constructor
Dim AddOrderDialog As New NewOrder(DirectCast _
(CustomersCombo.SelectedItem, Customers))
'Sets a relationship between the new Order and the new item
'added to the ListCollectionView
AddOrderDialog.Order = ord
AddOrderDialog.Owner = Me
'If the user clicks OK
If AddOrderDialog.ShowDialog() = True Then
'Saves changes to the collection
Me.OrderView.CommitNew()
Me.HelperClass.AddNewOrder(AddOrderDialog.Order)
'and refreshes the UI
Me.LoadOrders()
Else
'Otherwise cancels editing data
Me.OrderView.CancelNew()
End If
End Sub
Private Sub UpdateOrderButton_Click(ByVal sender As System.Object, _
ByVal e As System.Windows.RoutedEventArgs)
'First I need the selected ListView item, then I must convert this item
'into an Orders object that I can manipulate
Dim order As Orders = CType(Me.OrderView.CurrentItem(), Orders)
'then I can update my order
Try
Me.HelperClass.UpdateOrder(order)
Catch ex As ArgumentNullException
MessageBox.Show(ex.ToString)
Catch ex As DataServiceRequestException
MessageBox.Show(ex.ToString)
Finally
'and then populate again the ListView
Me.LoadOrders()
End Try
End Sub
Private Sub DeleteOrderButton_Click(ByVal sender As System.Object, _
ByVal e As System.Windows.RoutedEventArgs)
'First we obtain the current order from the ListCollectionView
Dim order As Orders = CType(Me.OrderView.CurrentItem(), Orders)
Try
'since the order from the ListCollectionView is data-bound
'to the ListView, we don't need to retrieve the selected item
'in the ListView itself.
Me.HelperClass.DeleteOrder(order)
Me.OrderView.Remove(order)
Catch ex As ArgumentNullException
MessageBox.Show(ex.ToString)
Catch ex As DataServiceRequestException
MessageBox.Show(ex.ToString)
Finally
Me.LoadOrders()
End Try
End Sub
Private Sub OrderDetailsButton_Click(ByVal sender As System.Object, _
ByVal e As System.Windows.RoutedEventArgs)
Dim order As Orders = DirectCast(Me.OrdersListView.SelectedItem, Orders)
Try
Dim ViewDetails As New ViewOrderDetails(order.OrderID)
ViewDetails.ShowDialog()
Catch ex As NullReferenceException
MessageBox.Show("No order was selected.")
End Try
End Sub
Private Sub Window1_Loaded(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) _
Handles Me.Loaded
'This is required for binding the original data source to the ListCollectionView
Me.OrderView = CType(CollectionViewSource.GetDefaultView(Me.DataContext), _
ListCollectionView)
End Sub
End Class
Listing 23
The ListCollectionView class offers methods for editing
items in the collection, such as EditItem and CommitEdit. These methods will
only be available when extending partial classes (in our sample the Order
partial class) with the IEditableObject
interface. Also it is a good practice implementing the INotifyPropertyChanged
interface (so that the UI can be notified about changes on data objects made
outside the UI itself) or using collections, such as the ObservableCollection,
that implement that interface. Another useful interface when working with data-binding
is the IDataErrorInfo,
which is very useful for providing information to the UI about data errors.
INotifyPropertyChanged and IDataErrorInfo, like IEditableObject, must be
implemented in a partial class related to the data object (e.g. Order). You can
find an example of implementing the three interfaces in this project from the MSDN Code Gallery.
It’s worth mentioning that the body of the Window1_Loaded
event handler retrieves a ListCollectionView based on the data source
(Me.DataContext) and then assigns the result to the OrderView object. Once this
is populated, we can manipulate data that it stores.
Notice also how we are handling exceptions and showing the
error messages through MessageBox
objects. Regarding this, please pay particular attention to the ArgumentNullException and NullReferenceException objects: the
user must select a row in the ListView
before executing a CRUD operation but if she selects no row and then clicks a
button, a null object is passed to the methods, so handling these kinds of
exceptions (each according to the particular method implementation) is really
important.
Testing the application
We are now ready to test our application. Press F5 and, when the orders’ list for the
first customer is shown, try to perform the following tasks:
- viewing the order details for a particular
order;
- adding a new order;
- updating the newly added order;
- deleting the order.
Figure 17 shows a moment of the application running.
.jpg)
Figure 17 – the running
sample application
Surely you could improve the code in several ways, first of
all by implementing custom data validation in the UI. In this article we needed
a fast way to understand how WPF clients can efficiently make use of ADO.NET
Data Services for data exchange so some improvements are left to your
particular needs.
Known issues
The current version of the
ADO.NET Data Services does not allow extension methods provided by Queryable and Enumerable
classes on the client side (except for the AsQueryable
and AsEnumerable ones). So please
take care of this limitation when architecting your applications. Additionally,
you could experience exceptions when deleting entities with associations. This
is due to a bug in the first release of ADO.NET Data Services which you can fix
by making sure this update
is installed. See this article
in the Knowledge Base for details.
Useful resources
ADO.NET Data
Services Portal
ADO.NET Data
Services (“Astoria”) Team Blog
ADO.NET Data
Services “How-do-I” videos
Article:
Using ADO.NET Data Services
ADO.NET
Data Services on Beth Massi’s blog
WPF
Forms Over Data Video series
About the author
Alessandro Del Sole is a Microsoft Visual Basic MVP and Team
Member in the Italian “Visual Basic Tips
& Tricks” Community. He writes lots of Italian
and English
language community articles and books about .NET development. He also enjoys
writing freeware and open-source developer tools. You can visit Alessandro’s Italian
language blog or his English language blog.