官术网_书友最值得收藏!

Separating the Data Access Layer

Now that we've had a look at providing a variety of functionality through our base classes and interfaces, let's investigate how we can provide the Separation of Concerns that is crucial when using the MVVM pattern. Once again, we turn to the humble interface to help us achieve this. Let's view a simplified example:

using System; 
using CompanyName.ApplicationName.DataModels; 
 
namespace CompanyName.ApplicationName.Models.Interfaces 
{ 
  public interface IDataProvider 
  { 
    User GetUser(Guid id); 
 
    bool SaveUser(User user); 
  } 
} 

We start off with a very simple interface. Of course, real applications will have a great many more methods than this, but the principle is the same, regardless of the complexity of the interface. So here, we just have a GetUser and a SaveUser method that our DataProvider classes need to implement. Now, let's look at the ApplicationDataProvider class:

using System; 
using System.Data.Linq; 
using System.Linq; 
using CompanyName.ApplicationName.DataModels; 
using CompanyName.ApplicationName.Models.Interfaces; 
 
namespace CompanyName.ApplicationName.Models.DataProviders 
{ 
  public class ApplicationDataProvider : IDataProvider 
  { 
    public ApplicationDataContext DataContext 
    { 
      get { return new ApplicationDataContext(); } 
    } 
 
    public User GetUser(Guid id) 
    { 
      DbUser dbUser = DataContext.DbUsers.SingleOrDefault(u => u.Id == id);  
      if (dbUser == null) return null; 
      return new User(dbUser.Id, dbUser.Name, dbUser.Age); 
    } 
 
    public bool SaveUser(User user) 
    { 
      using (ApplicationDataContext dataContext = DataContext) 
      { 
        DbUser dbUser = 
          dataContext.DbUsers.SingleOrDefault(u => u.Id == user.Id); 
        if (dbUser == null) return false; 
        dbUser.Name = user.Name; 
        dbUser.Age = user.Age; 
        dataContext.SubmitChanges(ConflictMode.FailOnFirstConflict); 
        return true; 
      } 
    } 
  } 
} 

This ApplicationDataProvider class uses some simple LINQ to SQL to query and update a database for the User specified by the id value provided. That means that this particular implementation of the interface requires a connection to a database. We want to avoid having this dependency when testing our application, so we'll need another implementation of the interface to use for testing purposes. Let's take a look at our mock implementation now:

using System; 
using CompanyName.ApplicationName.DataModels; 
using CompanyName.ApplicationName.Models.Interfaces; 
 
namespace Test.CompanyName.ApplicationName.Models.DataProviders 
{ 
  public class MockDataProvider : IDataProvider 
  { 
    public User GetUser(Guid id) 
    { 
      return new User(id, "James Smith", 25); 
    } 
 
    public bool SaveUser(User user) 
    { 
      return true; 
    } 
  } 
} 

In this MockDataProvider implementation of the IDataProvider interface, we can see that the data is just manually mocked. In fact, it just returns the one single User from the GetUser method and always returns true from the SaveUser method, so it's fairly useless.

In a real-world application, we would either utilize a mocking framework, or manually mock up some more substantial testing data. Still, this will suffice for the point that we are focusing on here. Now that we've seen the classes involved, let's look at how they might be used.

The idea is that we have some sort of DataController class or classes that sit between the IDataProvider interface and the View Model classes. The View Model classes request data from the DataController class and, in turn, it requests data through the interface.

It therefore mirrors the methods of the interface and typically introduces some extra functionality, such as feedback handling for example. Let's see what our simplified DataController class looks like:

using System; 
using CompanyName.ApplicationName.DataModels; 
using CompanyName.ApplicationName.Models.Interfaces; 
 
namespace CompanyName.ApplicationName.Models.DataControllers 
{ 
  public class DataController 
  { 
    private IDataProvider dataProvider; 
 
    public DataController(IDataProvider dataProvider) 
    { 
      DataProvider = dataProvider; 
    } 
 
    protected IDataProvider DataProvider 
    { 
      get { return dataProvider; } 
      private set { dataProvider = value; } 
    } 
 
    public User GetUser(Guid id) 
    { 
      return DataProvider.GetUser(id); 
    } 
 
    public bool SaveUser(User user) 
    { 
      return DataProvider.SaveUser(user); 
    } 
  } 
} 

As we can see, the DataController class has a private member variable of type IDataProviderwhich is populated in its constructor. It is this variable that is used to access the application data source. When the application is running, an instance of our ApplicationDataProvider class is used to instantiate the DataController class, and so our actual data source is used:

DataController dataController = 
  new DataController(new ApplicationDataProvider()); 

However, when we are testing our application, we can use an instance of our MockDataProvider class to instantiate the DataController class instead, thereby eliminating our dependency on the actual data source:

DataController dataController = new DataController(new MockDataProvider());

In this way, we can swap out the code that provides the data for the View Models, while keeping the rest of the code unchanged. This enables us to test the code in the View Models without having to be connected to our actual data storage device. In the next section, we'll see better ways to initialize these classes, but for now, let's see what else our DataController class could do for us.

Interfaces become more useful when they are used by parts of the application framework, other than the implementing classes. Apart from than defining some auditing properties and having the possibility of outputting their values, our earlier IAuditable interface example is not overly useful. We could however, extend its functionality further in our DataController class by automatically updating its values. We'll need to add some more members to achieve this:

using CompanyName.ApplicationName.DataModels.Interfaces; 
    
... 
    
public User CurrentUser { get; set; } 
    
... 
    
private void SetAuditUpdateFields<T>(T dataModel) where T : IAuditable 
{ 
  dataModel.Auditable.UpdatedOn = DateTime.Now; 
  dataModel.Auditable.UpdatedBy = CurrentUser; 
  return dataModel; 
} 

We first need to add a property of type User that we will use to set the value of the current user of the application. This can be set as new users login to the application. Next, we need a method to update the "updated" values of our IAuditable interface. Again, we add a generic type constraint to ensure that only objects that implement our interface can be passed into this method. The result of this is that the developers that use our application framework can easily update these values:

public bool SaveUser(User user) 
{ 
  return DataProvider.SaveUser(SetAuditUpdateFields(user)); 
} 

We could add a similar method to set the "created" audit properties when adding new objects:

public bool AddUser(User user) 
{ 
  return DataProvider.AddUser(SetAuditCreateFields(user)); 
} 
    
... 
    
private void SetAuditCreateFields<T>(T dataModel) where T : IAuditable 
{ 
  dataModel.Auditable.CreatedOn = DateTime.Now; 
  dataModel.Auditable.CreatedBy = CurrentUser; 
  return dataModel; 
} 

Continuing this example, we could extend the constructor of our DataController class to accept a User input parameter that we can use to set our CurrentUser property with:

public DataController(IDataProvider dataProvider, User currentUser) 
{ 
  DataProvider = dataProvider; 
  CurrentUser = currentUser; 
} 

We could then expose our data source to our View Models through their base class using a CurrentUser property in the StateManager class and the DependencyManager class that we'll see in the following sections:

protected DataController Model 
{ 
  get { return new DataController( 
    DependencyManager.Instance.Resolve<IDataProvider>(), 
    StateManager.CurrentUser); } 
} 

Essentially, anything, that we need to do to the data coming from our application data source can be achieved in a single DataController class. However, if we require several different modifications, then we could alternatively create several controller classes and chain them together, with each performing their separate tasks in turn.

As they could all implement the same methods, they could all potentially implement the same interface:

We'll see an example of this in Chapter 10, Completing That Great User Experience, but now that we have a good idea on how best to setup our application data source connections to provide the separation required by the MVVM pattern, we can focus on the next way of building functionality into our framework. Let's move on to discover how we can plug more complex and/or specialized functionality into our framework.

主站蜘蛛池模板: 宜兰市| 全州县| 道真| 射阳县| 绥江县| 松桃| 康保县| 诸暨市| 栾川县| 望城县| 绿春县| 旬邑县| 农安县| 惠安县| 海门市| 宝山区| 襄樊市| 伊宁县| 宁夏| 姜堰市| 拉萨市| 广宁县| 开平市| 潜山县| 阳原县| 余庆县| 临沭县| 鞍山市| 子洲县| 兴义市| 闻喜县| 原平市| 女性| 潢川县| 格尔木市| 清新县| 大方县| 石林| 湘潭市| 台中市| 卢湾区|