Let's retake our journey through the MVVM pattern in WPF 4, with Visual Basic 2010, against ADO.NET Entity Framework. Last time we saw how to implement a Messenger class, describing what it is for although we'll see such a class in action in my next post. In this article we will write some code for data access which is important to understand how complex can be building applications based on the MVVM but also how many benefits it can bring into your developer life.
Problem of the day: does the ViewModel know what's the data source?
I confessed that I had to delay the blog post series about MVVM and the Entity Framework because while I was studying the pattern I understood that there was something wrong in my approach and that I was making some mistakes. We are at the point in which we have some data, a data access layer, an object model that allows working with the data. In theory it would be enough writing a ViewModel able of performing data access operations by implementing the appropriate commands.
But here is the real problem: if we think of abstraction, if the ViewModel could interact directly against an EDM it would strictly depend on the EDM itself. But one of the most important purposes of the MVVM is bringing the abstraction level between layers to the max and thus such an approach is not good. The ViewModel should be so indipendent that it should be able of dialing with any data source without suffering in case the data source itself is replaced. In other words, if today I work with XML and tomorrow I will decide to replace it with SQL Server, I should have a so versatile ViewModel that it will not change if my data source changes and, most of all, this approach also allows Views to work with no code edits. Can we do this? Yes, of course. We need just some more work :-)
Thinking about services
The goal is answering the following question: how do I create a ViewModel that does not know what is the underlying data source but that is able of reading and editing such a data source? The problem can be solved by adding a service layer that works like this:
-
We define an interface named by convention as IxxxDataService, where XXX is the name of the data to represent (e.g. ICustomerDataService). Such an interface defines members that will interact with the data source like queries, insert/update/delete, etc.
-
We declare a class, named by convention as xxxDataService, where XXX is the name of the data to represent (e.g. CustomerDataService), that implements the previous interface and that executes the actual work against data.
-
We write a class named ServiceLocator, which is responsible for passing to callers all the tools for working with the data source, providing separation logic.
-
We write the ViewModel, which will invoke members from the xxxDataService class, without knowing what the underlying data source is.
What is the advantage of all the above mentioned infrastructure? Imagine that your ViewModel exposes a command for saving data. The ViewModel will expose a command named Save, that will invoke the same-named command from the service class. In this way the ViewModel does not strictly depend on the data source, because it does not know what the underlying source is. Moreover, it exposes a "conventional" command and is able of reaching the objective (that is, saving data). Maybe you are a little bit confused after my discussion. But don't worry, in next post I will begin talking about the ViewModel and everything will be clearer. Just a clarification: you should generally implement as many interfaces/classes as many models you have. We are now ready to implement a service layer.
Refactoring? Yes, of course! But in the final post!
Probably at the end of this blog post, the most experienced developers will point out that some refactoring on our code should be performed. This is true but I have in mind a specific blog post on refactoring the full project, so please don't care about this for now.
Implementing the ServiceLocator class
As I mentioned before, it's necessary implementing a class named by convention as ServiceLocator which offers the possibility of registering interfaces from the service layer and that will also allow getting the instances of the service classes. Such a class is also explained inside the Prism documentation, so you can check this out for details. At the moment I just need to describe the class implementation and its methods. With that said let's add a new class named ServiceLocator to the Services project subfolder. This is the code:
Public Class ServiceLocator
Implements IServiceProvider
Private services As New Dictionary(Of Type, Object)()
Public Function GetService(Of T)() As T
Return CType(GetService(GetType(T)), T)
End Function
Public Function RegisterService(Of T)(ByVal service As T, ByVal overwriteIfExists As Boolean) As Boolean
SyncLock services
If Not services.ContainsKey(GetType(T)) Then
services.Add(GetType(T), service)
Return True
ElseIf overwriteIfExists Then
services(GetType(T)) = service
Return True
End If
End SyncLock
Return False
End Function
Public Function RegisterService(Of T)(ByVal service As T) As Boolean
Return RegisterService(Of T)(service, True)
End Function
Public Function GetService(ByVal serviceType As Type) As Object Implements IServiceProvider.GetService
SyncLock services
If services.ContainsKey(serviceType) Then
Return services(serviceType)
End If
End SyncLock
Return Nothing
End Function
End Class
Basically the class implements the IServiceProvider interface and exposes two methods: RegisterService, which registers a type as a service provider, and GetService which retrieves the instance of the service class that makes the actual work against data. Once we have this class, we can define interfaces. There is just one minor step that we need to do before: instantiating the ObjectContext.
Declaring the ObjectContext
We need a place where to create the instance of the ObjectContext which is named NorthwindEntities in the current project. This cannot be done inside the ViewModel in order to preserve the abstraction logic. This could be done at the service layer level, but that is not the perfect choice. We can consider declaring a project level variable instead; so let's add a new module named Helper to the Helpers subfolder and then let's write the following code:
Module Helper
Public Northwind As New NorthwindEntities
End Module
We are now really ready for providing interfaces.
Implementing IDataService interfaces
If you remember the figure where I showed how the application will look like at the end of the post series, you may remember how on the left side there is the customers list that the code retrieves from the Customers table in the database. With regard to customers, we don't need anything else so we just need to simply implement an interface defining one method that retrieves the list of customers. Let's add a new interface named ICustomerDataService to the Services subfolder. This is the code:
Public Interface ICustomerDataService
Function GetAllCustomers() As IQueryable(Of Customer)
End Interface
As you can see the interface defines a simple method called GetAllCustomers that, once implemented, will return the full list of customers under the form of an IQueryable(Of Customer). Now it's time for a couple of considerations: the method returns simply an IQueryable instead of an ObjectQuery(Of T) which is typical in Entity Framework. This is appropriate because the result does not depend on the underlying data source. Moreover, one day I could replace the Customer type exposed by my EDM with a custom Customer business object; with the current approach, the ViewModel that receives the result will not be affected at all.
As I mentioned before, for each ViewModel you should implement a service interface. We are working with customers and ordes, so now we need an interface for the latter data type. The new interface is quite interesting because we need to implement:
-
a method that retrieves the orders list. We will provide two overloads, one for getting the list as an IQueryable and one for getting the list of orders for a given Customer under the form of an ObservableCollection because we are also interested in editing data.
-
a method for saving data
-
a method for adding new orders
-
a method for removing existing orders
-
methods for navigating between orders
With that said, the new interface is called IOrderDataService and we add it to the Services project subfolder. This is the code:
Imports System.Collections.ObjectModel
Public Interface IOrderDataService
Function GetOrderDetailsByOrderId(ByVal ID As Integer) As ObservableCollection(Of Order_Detail)
Function GetAllOrders() As IQueryable(Of Order)
Function GetAllOrders(ByVal customerID As String) As ObservableCollection(Of Order)
Sub Save()
Sub Delete(ByVal dataSource As Object)
Sub Insert(ByVal dataSource As Object, ByVal selectedCustomer As Customer)
Sub MoveToNext(ByVal dataSource As Object)
Sub MoveToPrevious(ByVal dataSource As Object)
End Interface
I will better explain some details while implementing both interfaces; for now I would like to underline that the first overload of GetAllOrders, which returns IQueryable, is just implemented for future utilization even if actually we will not use it inside the current application. Other methods receive, as an argument, the data source to work with. Such a data source is passed as an Object. This is useful so that methods can accept different data sources and the appropriate conversions are performed inside service classes.
Implementing service classes
Once the interfaces are defined, we need some classes that implement those interfaces. First of all, let's create a new class named CustomerDataService that implements the related interface. Such a class should be added to the Services subfolder and is made of the following code:
Public Class CustomerDataService
Implements ICustomerDataService
Public Function GetAllCustomers() As IQueryable(Of Customer) Implements ICustomerDataService.GetAllCustomers
Return Northwind.Customers.Include("Orders")
End Function
End Class
The method here returns a pure .NET type, which is IQueryable and that will be received by the ViewModel. The method body now returns the result of a query executed against an Entity Data Model, but in the future you might want to replace the data source and return the result of a similar query executed against an XML document. With this approach the ViewModel will continue receiving an IQueryable without knowing what changes occurred at the data source level. While this class is pretty simple, the service class that implements IOrderDataService is more complex although the basic idea is the same. With that said let's add a new class named OrderDataService whose code is the following:
Imports System.Data, System.Windows.Data
Imports System.Data.Objects
Imports System.ComponentModel
Imports System.Collections.ObjectModel
Public Class OrderDataService
Implements IOrderDataService
Public Function GetOrderDetailsByOrderID(ByVal ID As Integer) As ObservableCollection(Of Order_Detail) Implements IOrderDataService.GetOrderDetailsByOrderId
Dim OrderDetailsQuery As System.Data.Objects.ObjectQuery(Of Order_Detail) = CType((From det In Northwind.Order_Details
Where det.OrderID = ID
Select det),
Global.System.Data.Objects.ObjectQuery(Of Order_Detail))
Return New ObservableCollection(Of Order_Detail)(OrderDetailsQuery)
End Function
Public Sub Insert(ByVal dataSource As Object, ByVal customer As Customer) Implements IOrderDataService.Insert
Dim newOrder As Order
Dim tp = dataSource.GetType
Select Case tp.Name
Case Is = "ListCollectionView"
Dim source = CType(dataSource, ListCollectionView)
newOrder = CType(source.AddNew, Order)
newOrder.Customer = customer
'Add a new order so that the data binding
'engine will invoke the ErrorTemplate
source.CommitNew()
Case Is = "BindingListCollectionView"
Dim source = CType(dataSource, BindingListCollectionView)
newOrder = CType(source.AddNew, Order)
newOrder.Customer = customer
'Add a new order so that the data binding
'engine will invoke the ErrorTemplate
source.CommitNew()
Case Else
Throw New InvalidOperationException("Data source is of a type CollectionView which does not support adding items")
End Select
End Sub
Public Sub Save() Implements IOrderDataService.Save
Try
Northwind.SaveChanges()
Catch ex As OptimisticConcurrencyException
'Handling concurrency
Northwind.Refresh(Objects.RefreshMode.ClientWins,
Northwind.Orders)
Northwind.SaveChanges()
Catch ex As Exception
Throw
End Try
End Sub
Public Function GetAllOrders() As IQueryable(Of Order) Implements IOrderDataService.GetAllOrders
Return Northwind.Orders
End Function
Public Function GetAllOrders(ByVal customerID As String) As ObservableCollection(Of Order) Implements IOrderDataService.GetAllOrders
Dim query = From ord In Northwind.Orders.Include("Customer")
Where ord.CustomerID = customerID
Select ord
Return New ObservableCollection(Of Order)(query)
End Function
Public Sub MoveToNext(ByVal dataSource As Object) Implements IOrderDataService.MoveToNext
CType(dataSource, CollectionViewSource).View.MoveCurrentToNext()
End Sub
Public Sub MoveToPrevious(ByVal dataSource As Object) Implements IOrderDataService.MoveToPrevious
CType(dataSource, CollectionViewSource).View.MoveCurrentToPrevious()
End Sub
Public Sub Delete(ByVal dataSource As Object) Implements IOrderDataService.Delete
Dim tp = dataSource.GetType
Select Case tp.Name
Case Is = "ListCollectionView"
Dim source = CType(dataSource, ListCollectionView)
source.Remove(source.CurrentItem)
Case Is = "BindingListCollectionView"
Dim source = CType(dataSource, BindingListCollectionView)
source.Remove(source.CurrentItem)
Case Else
Throw New InvalidOperationException("Data source is of a type that does not support removing items")
End Select
End Sub
End Class
Let's make the following considerations:
-
The GetOrderDetailsByOrderId method returns the list of Order Details for the specified order. You should actually implement a method like this inside a specific service related to Order Details, but since this is the one and only operation that we make against such data, we can place it here. The result is an ObservableCollection so that we can have full data-binding support.
-
The Insert method allows adding a new order to the existing data. The first argument is the data source that will store the new element. It is of type Object but the method body analyzes the actual type and performs the appropriate conversion into a ListCollectionView or BindingListCollectionView depending on the caller. This is because we already know that we will specifically work with this kind of objects coming from the ViewModel. Of course you could extend the code by implementing analysis and conversion to ObservableCollection but this is left to you as an exercise. Working with View objects is convenient because they offer members for working with data, such as browsing, grouping and filtering. The second argument is just the instance of the customer that the new order is associated to.
-
The overloads implementations for GetAllOrders is quite simple. The second one queries the list of orders belonging to the specifdied customer and returns the result under the form of an ObservableCollection. This overload will be used in our ViewModel.
-
The Save method is easy. It saves data to the database, checks for concurrency and throws exceptions when required.
-
The Delete method works like Insert but it differs in that it removes an order from the supplied data source.
-
The MoveToNext and MoveToPrevious methods allow browsing the orders list and they assume that the data source is browsable, thus of type ICollectionView. For this reason there is a direct conversion and invocation to the appropriate methods.
Refactoring the code is required, but this will be done later. Today we have written a lot of code and learned a lot of new concepts.
End of the story
Part 7 has been really hard. We have:
-
described how the ViewModel must have a high level of abstraction if compared to the data source, acting against data without knowing the actual type of the data source
-
described how this is possible by implementing a service layer
-
implemented a ServiceLocator class that will be responsible of registering services and retrieving their instances
-
implemented service interfaces and classes that will allow working with data, sending the result to the ViewModel
In next post I will definitely begin discussing ViewModels. We are quite near to the end of the work.
Alessandro