DotVVM In Real-World Apps - Generic CRUD - Part Two

In the previous article, I was writing about building basic CRUD scenarios using DotVVM, an open-source MVVM framework for the line of business web apps.

In this part, I would like to demonstrate how to make many CRUD screens using generic types in C# and how to build universal base classes with a few extensibility points. It is useful in larger applications with many CRUD screens as it significantly decreases the amount of code you need to write.

Again, I will be using the NorthwindStore DotVVM Demo project.

DotVVM uses C# classes as ViewModels. I would like to have two base classes for my ViewModels – one for the list page and one for the detail page.

The list page should allow the user to see data in the GridView control, with filtering, sorting, and paging. The records can also be deleted from the list page.

The details page should enable the user to insert or update a particular record.

Business Layer and Interfaces

The ViewModels must access the data somehow. In the demo project, I have a set of classes called Facades. They provide everything that I need to the viewmodels. How they work is out of the scope of this article; the important thing is that these Facades implement the following generic interface,

  1. public interface IListFacade<TDTO, TKey>  
  2. {  
  3.     void FillDataSet(GridViewDataSet<TDTO> items);  
  4.     void Delete(TKey id);  
  5. }  

The FillDataSet method looks at the GridViewDataSet settings for paging and sorting and apply these settings to the SQL query.

The Delete method just deletes the record from the database, or mark it as deleted.

Notice that the façade has two types of arguments.

  • TDTO (DTO stands for Data Transfer Object) is a type of object I want to show in the grid.

  • TKey is a type of the primary key of the object. In most cases, it would be int, but there might be a situation where the key is different.

The façade for the detail page implements the following interface,

  1. public interface IDetailFacade<TDTO, TKey>  
  2. {  
  3.     TDTO GetDetail(TKey id);  
  4.     TDTO InitializeNew();  
  5.     TDTO Save(TDTO item);  
  6. }  

The GetDetail method will find the record by its primary key and return the DTO with all data I need.

The InitializeNew method returns a new object with default values of properties. It is called whenever the user wants to insert a new record. Please note that this method does not store anything in the database – it only initializes a new DTO with default values.

The Save method takes care of both insert and update of the object. Since the database can fill some properties automatically (generate the primary key for example), the Save method returns the most current version of the DTO.

It does not matter how the facades are implemented. In my case, they are using Entity Framework and AutoMapper to work with the database and build DTO representations of entities.

As I wrote in the previous part – it is not a good idea to work with Entity Framework entities in the user interface and place them in the viewmodel – it could expose columns which the user should not see, as they are serialized in JSON, and it would cause a lot of other issues.

The DTOs also allow us to make a REST API and expose the same DTOs using the API, which may be useful when you need to build a mobile application.

Generic ViewModel for the list pages

Now I have facades which implement generic interfaces so that I can build generic viewmodels.

The base viewmodel for the list page can look like this,

  1. public abstract class ListPageViewModel<TDTO, TKey> : DotvvmViewModelBase   
  2.      where TDTO : new()  
  3.  {  
  4.      [Bind(Direction.None)]  
  5.      public IListFacade<TDTO, TKey> Facade { get; }  
  6.   
  7.      public ListPageViewModel(IListFacade<TDTO, TKey> facade)  
  8.      {  
  9.          this.Facade = facade;  
  10.      }  
  11.        
  12.   
  13.      [Bind(Direction.None)]  
  14.      public abstract ISortingOptions DefaultSortOptions { get; }  
  15.   
  16.      public GridViewDataSet<TDTO> Items { get; set; }  
  17.   
  18.   
  19.      public override Task Init()  
  20.      {  
  21.          Items = new BpGridViewDataSet<TDTO>()  
  22.          {  
  23.              PagingOptions =  
  24.              {  
  25.                  PageSize = 50  
  26.              },  
  27.              SortingOptions = DefaultSortOptions  
  28.          };  
  29.   
  30.          return base.Init();  
  31.      }  
  32.   
  33.   
  34.      public override Task PreRender()  
  35.      {  
  36.          if (!Context.IsPostBack || Items.IsRefreshRequired)  
  37.          {  
  38.              LoadData();  
  39.          }  
  40.   
  41.          return base.PreRender();  
  42.      }  
  43.   
  44.      private void LoadData()  
  45.      {  
  46.          OnDataLoading();  
  47.          Facade.FillDataSet(Items);  
  48.          OnDataLoaded();  
  49.      }  
  50.        
  51.      public void Delete(TKey id)  
  52.      {  
  53.          OnItemDeleting(id);  
  54.          Facade.Delete(id);  
  55.          OnItemDeleted(id);  
  56.      }  
  57.   
  58.      protected virtual void OnDataLoading()  
  59.      {  
  60.      }  
  61.   
  62.      protected virtual void OnDataLoaded()  
  63.      {  
  64.      }  
  65.   
  66.      protected virtual void OnItemDeleting(TKey id)  
  67.      {  
  68.      }  
  69.   
  70.      protected virtual void OnItemDeleted(TKey id)  
  71.      {  
  72.      }  
  73.  }  

First, look at the constructor of the viewmodel. It accepts IListFacade<TDTO, TKey> as a parameter. I will have all my facades registered in the service collection on application start so that the instance can be injected in the viewmodel automatically by DotVVM. Thanks to this, I do not need to use the façade constructor or call Dispose when the HTTP request ends.

After the constructor, I declare an abstract property which defines the default sort order of the records, and the GridViewDataSet itself. The GridViewDateSet is initialized in the Init phase.

In the PreRender phase, I am checking whether this is the first request to the page (IsPostBack is false) and whether the refresh is requested. The GridViewDataSet contains a flag that indicates that it should be refreshed, for example when the user changed the sort order.

Finally, the LoadData method calls FillDataSet on the façade. Similarly, the Delete method also calls the corresponding method on the façade. There are four extensibility points which can be overridden in derived classes to add extra functionality. In real-world applications, most CRUDs still have some extra functionality.

These extensibility points are empty virtual methods (so they could be overridden) that are called before and after the data is loaded, and before and after a record is deleted.

Using the generic list page

When I want to build a list page, I can now just inherit from the base class.

  1. public class ProductListViewModel : ListPageViewModel<ProductListDTO, int>  
  2. {  
  3.   
  4.     public ProductListViewModel(AdminProductsFacade facade) : base(facade)  
  5.     {  
  6.     }  
  7.   
  8.     public override ISortingOptions DefaultSortOptions => new SortingOptions()  
  9.     {  
  10.         SortExpression = nameof(ProductListDTO.ProductName)  
  11.     };  
  12. }  

When you look at the generic parameters, you can see that I am using ProductListDTO as the DTO.

This class contains only the properties I need to display in the GridView control. The database table may contain some columns that aren’t necessary – SupplierID and CategoryID for example. Moreover, there are some columns in other tables which can be required, like the name of the supplier and category.

The ProductListDTO class is declared like this,

  1. public class ProductListDTO  
  2. {  
  3.     public int Id { get; set; }  
  4.     public string ProductName { get; set; }  
  5.     public string SupplierName { get; set; }  
  6.     public string CategoryName { get; set; }  
  7.     public string QuantityPerUnit { get; set; }  
  8.     public decimal? UnitPrice { get; set; }  
  9.     public short? UnitsInStock { get; set; }  
  10.     public short? UnitsOnOrder { get; set; }  
  11.     public short? ReorderLevel { get; set; }  
  12.     public bool Discontinued { get; set; }  
  13. }  

It is very similar to the Product entity in the NorthwindStore.DAL project. But if you look closer, you will notice that SupplierID and CategoryID properties are missing. Instead, I have added SupplierName and CategoryName.

In the business layer, I am using AutoMapper to map these properties. You can look in the ProductMapping class to see how the mapping is done,

  1. mapper.CreateMap<Products, ProductListDTO>()  
  2.     .ForMember(p => p.SupplierName, m => m.MapFrom(p => p.Supplier.CompanyName))  
  3.     .ForMember(p => p.CategoryName, m => m.MapFrom(p => p.Category.CategoryName));  

Notice that I have only specified these extra properties in the mapping configuration; the other ones are mapped automatically because they have the same names as in the Product entity.

The reason why I mention this is that I am using a different DTO for the detail page. In real-world applications, there may be much more fields in the detail page than in the list page. Having two different DTOs make the list page more efficient as I do not need to load data that I do not use. Thanks to AutoMapper, it is not a big deal to have multiple DTO classes for the same database table. I find it much more practical as I can change one DTO without breaking other places in the application which work with the same database table.

In the demo application, there are another two properties which are inherited from the viewmodel of the master page. They define the page title and the menu category which should be highlighted.

  1. public override string PageTitle => "Categories";  
  2. public override string HighlightedMenuPath => "Categories";  

Generic ViewModel for detail page

The viewmodel for detail page is implemented quite similarly. Again, it requests the facade in the constructor.

  1. public abstract class DetailPageViewModel<TDTO, TKey> : AdminViewModel  
  2.     where TDTO : IEntity<TKey>  
  3. {  
  4.     [Bind(Direction.None)]  
  5.     public IDetailFacade<TDTO, TKey> Facade { get; }  
  6.   
  7.     public DetailPageViewModel(IDetailFacade<TDTO, TKey> facade)  
  8.     {  
  9.         this.Facade = facade;  
  10.     }  
  11.   
  12.   
  13.     public abstract string ListPageRouteName { get; }  
  14.   
  15.   
  16.     public TKey CurrentItemId { get; set; }  
  17.   
  18.     public bool IsNew => Equals(CurrentItemId, default(TKey));  
  19.   
  20.     public TDTO CurrentItem { get; set; }  
  21.   
  22.   
  23.     public override Task Init()  
  24.     {  
  25.         if (Context.Parameters.ContainsKey("Id"))  
  26.         {  
  27.             CurrentItemId = (TKey) Convert.ChangeType(Context.Parameters["Id"], typeof(TKey));  
  28.         }  
  29.   
  30.         return base.Init();  
  31.     }  
  32.   
  33.     public override Task PreRender()  
  34.     {  
  35.         if (!Context.IsPostBack)  
  36.         {  
  37.             if (!IsNew)  
  38.             {  
  39.                 CurrentItem = Facade.GetDetail(CurrentItemId);  
  40.             }  
  41.             else  
  42.             {  
  43.                 CurrentItem = Facade.InitializeNew();  
  44.             }  
  45.             OnItemLoaded();  
  46.         }  
  47.   
  48.         return base.PreRender();  
  49.     }  
  50.   
  51.   
  52.     protected virtual void OnItemLoaded()  
  53.     {  
  54.     }  
  55.   
  56.     protected virtual void OnItemSaving()  
  57.     {  
  58.     }  
  59.   
  60.     protected virtual void OnItemSaved()  
  61.     {  
  62.     }  
  63.   
  64.   
  65.     public void Save()  
  66.     {  
  67.         OnItemSaving();  
  68.   
  69.         CurrentItem = Facade.Save(CurrentItem);  
  70.         CurrentItemId = CurrentItem.Id;  
  71.   
  72.         OnItemSaved();  
  73.   
  74.         Context.RedirectToRoute(ListPageRouteName);  
  75.     }  
  76.   
  77.     public void Cancel()  
  78.     {  
  79.         Context.RedirectToRoute(ListPageRouteName);  
  80.     }  
  81.   
  82. }  

The ListPageRouteName property specifies the name of the route of the list page. The viewmodel will redirect to it after the user saves the changes.

The CurrentItemId property is set in the Init phase with the value of the route parameter named “id”. If the parameter is not present, the page allows inserting a new record.

The CurrentItem property contains the DTO itself. No matter how complex the DTO is, how many nested objects or collections it contains, it is not difficult to build a user interface for that DTO with DotVVM and its two-way data-binding.

In the PreRender method, I am either initializing the new object for inserting or loading an existing object by the ID specified in the route. Both options simply call the appropriate methods in the façade. There is an extensibility point that can be used to perform an additional action after the DTO is initialized or loaded.

The Save method calls the corresponding method on the façade and again, it invokes an empty virtual method before and after the data is saved. Then, it redirects the user to the list page.

Using the generic detail page

The products page uses the base viewmodel to implement the base CRUD functionality,

  1. public class ProductDetailViewModel : DetailPageViewModel<ProductDetailDTO, int>  
  2. {  
  3.     private readonly BaseListsFacade baseListsFacade;  
  4.   
  5.     public ProductDetailViewModel(AdminProductsFacade facade) : base(facade)  
  6.     {  
  7.         this.baseListsFacade = baseListsFacade;  
  8.     }  
  9.   
  10.     public override string ListPageRouteName => "Admin_ProductList";  
  11.   
  12. }  

Notice that I am using ProductDetailDTO instead of ProductListDTO. As I have already pointed out, the DTO used in the detail page can be very different, and it can also manage related records – it can contain nested objects or collections.

My ProductDetailDTO does not contain SupplierName and CategoryName. Instead, there are the original SupplierID and CategoryID properties that can also be found in the Product entity. This is because I am using ComboBox controls in the user interface, and they work better with the ID properties.

The only thing they need in the viewmodel are the collections of suppliers and categories. I can add them in the viewmodel using the following code snippet,

  1. [Bind(Direction.ServerToClientFirstRequest)]  
  2. public List<CategoryBasicDTO> Categories => baseListsFacade.GetCategories();  
  3.   
  4. [Bind(Direction.ServerToClientFirstRequest)]  
  5. public List<SupplierBasicDTO> Suppliers => baseListsFacade.GetSuppliers();  

Notice that these collections are decorated with the Bind attribute. This attribute tells DotVVM that these properties should only be transferred on the first request. There is no need to send them to the server on postback or back to the client as they cannot be changed. It helps DotVVM to optimize the amount of data transferred between the server and the client.

The ComboBox controls that select the supplier and category look like this,

  1. <div class="form-field">  
  2.     <label>Category</label>  
  3.     <div>  
  4.         <bp:DropDownList DataSource="{value: _root.Categories}"  
  5.             SelectedValue="{value: CategoryId}"   
  6.             ItemTextBinding="{value: CategoryName}"   
  7.             ItemValueBinding="{value: Id}" />  
  8.     </div>  
  9. </div>  

The DataSource property points to the list of all categories. Because the form uses DataContext property and thus, all bindings are evaluated on CurrentItem property, I need to use _root.Categories to tell DotVVM to look for the collection on the root viewmodel.

The SelectedValue property references the property which holds the value selected by the user.

The ItemTextBinding tells the control which property from the Categories collection should be displayed in the ComboBox, and the ItemValueBinding tells the control which property is used as the selected value.

Conclusion

I described the principles of making universal generic viewmodels for implementing CRUD screens in DotVVM. I have installed several extensibility points in the viewmodels so the pages may contain a custom logic.

Of course, these viewmodels should be adapted to match your application, so feel free to add more functionality or change anything if it makes sense.

In the next part of this article series, I’d like to show some more advanced customizations of this generic CRUD, that can be found in the NorthwindStore Demo app.

Resources


Similar Articles