DotVVM In Real-World Apps - Part One - Basic CRUD

A few months ago, I wrote an article on DotVVM, an open source MVVM framework that allows building ASP.NET web apps without knowledge of JavaScript.

In this article series, I would like to show how DotVVM works in more complicated web applications and demonstrate useful features you may appreciate in your next project – an admin site, intranet portal or a CRM/ERP web app.

Sample Application

I will be using NorthwindStore DotVVM Demo project in this article to demonstrate how DotVVM can be used in real-world scenarios.

If you want to try the project, follow the instructions in the README.

Application Layers

The sample application consists of 3 layers,

  • DAL
    this project is the Data Access Layer and contains the Entity Framework Core model.

  • BL
    this project is the Business Layer of the application. It provides all the functionality required by the user interface and returns all data in a format that the UI needs.

  • App
    this project is the Presentation Layer. It is the DotVVM web app itself and contains all the pages, CSS files and so on.

The purpose of this article is not to describe how the DAL and BL works. If you are interested in it, you can look at the source code – these projects use Riganti.Utils.Infrastructure library, which is also developed on GitHub.

From the DotVVM perspective, there are a couple of important things for the Business Layer:

  • The BL exposes Facades which provide all functions that DotVVM pages need. A facade is a standard C# class with methods.
  • All Facade methods accept and return Data Transfer Objects (DTOs), which are plain C# classes with properties and without any dependencies on Entity Framework.
  • The Presentation Layer never works with Entity Framework entities; they are used only in the BL and DAL. The BL is responsible for the translation of the entities to DTOs and back.
DotVVM Application

From now on, I will be working with the NorthwindStore.App project. That is where the DotVVM pages and their ViewModels reside.

GridView Control

DotVVM contains a control called GridView. This control can render data in a table and supports inline editing, sorting, paging and other useful features.

GridView can be bound to any .NET collection, or to a special object called GridViewDataSet<T>. This class is a part of DotVVM and provides everything you may need to implement server-side paging and sorting.

The GridViewDataSet<T> contains the following properties:

  • Items (List<T>) is a collection of records displayed on the current page.
  • PagingOptions contains metadata for paging (PageIndex, PageSize, TotalItemsCount).
  • SortingOptions contains metadata for sorting (SortExpression, SortDescending).
  • RowEditOptions contains information about a currently edited row.

You can work with the GridViewDataSet in two ways,

  • If you are using Entity Framework or other databases which supports IQueryable, you can just call dataSet.LoadFromQueryable(queryable).

    The GridViewDataSet will apply paging and sorting automatically, based on the values in its PagingOptions and SortingOptions

  • You can load data in the GridViewDataSet,

    1. First, look in the PagingOptions and SortingOptions for the information you need to perform the query.
    2. Get the data.
    3. Add them in the Items collection.
    4. If you use paging, make sure you set the total number of records in the PagingOptions.TotalItemsCount property, so the pager controls can calculate how many pages they need.

The sample application contains two pages that work with regions – RegionList.dothtml and RegionDetail.dothtml.

The viewmodel for a RegionList page looks like this,

  1. public class RegionListViewModel: AdminViewModel {  
  2.     private readonly AdminRegionsFacade pageFacade;  
  3.     public RegionListViewModel(AdminRegionsFacade pageFacade) {  
  4.         this.pageFacade = pageFacade;  
  5.         Regions = new BpGridViewDataSet < RegionDTO > () {  
  6.             PagingOptions = {  
  7.                     PageSize = 50  
  8.                 },  
  9.                 SortingOptions = {  
  10.                     SortExpression = nameof(RegionDTO.Id)  
  11.                 }  
  12.         };  
  13.     }  
  14.     public GridViewDataSet < RegionDTO > Regions {  
  15.         get;  
  16.         set;  
  17.     }  
  18.     public override Task PreRender() {  
  19.         pageFacade.FillDataSet(Regions);  
  20.         return base.PreRender();  
  21.     }  
  22.     public void Delete(int id) {  
  23.         pageFacade.Delete(id);  
  24.     }  
  25. }  

Notice that the ViewModel constructor receives the instance of the facade for working with Regions. It works because of the Dependency Injection support in ASP.NET Core and DotVVM: all facades are registered in the service collection so they can be injected to constructor parameters.

In the constructor, I initialize the dataset with default page size and default sort order.

In the PreRender method, I pass the dataset to the facade which loads data in it.

The façade belongs to the Business Layer, which shouldn’t reference DotVVM because DotVVM is a presentation library. However, there is a package called DotVVM.Core which contains several interfaces (including IGridViewDataSet) which are useful in the Business Layer. That is why I can pass the dataset to the facade method to have it loaded.

I am using RegionDTO objects in my dataset. As I have mentioned before, the BL returns DTO objects, which are plain C# classes. They are JSON-serializable (so they can be placed in DotVVM ViewModels), they don’t depend on Entity Framework context and don’t contain any circular references.

  1. public class RegionDTO: IEntity < int > {  
  2.     public int Id {  
  3.         get;  
  4.         set;  
  5.     }  
  6.     [Required]  
  7.     public string RegionDescription {  
  8.         get;  
  9.         set;  
  10.     }  
  11. }  

In this case, it may look useless not to use the Entity Framework entity directly, as it is pretty much the same. However, there are other entities which are much more complicated, and there will be many differences between the entities and their DTOs. For example, there will be extra properties with data from linked tables, or some properties may be missing from the DTO as they are used in the user interface.

I am used to making specialized DTOs every place where they appear. When I display a list of customers, I need to see only some columns. When I am editing a customer, I will probably need all the columns from the Customers table. When I look at the report about the customer, there will also be a slightly different set of columns. Because there are typically more DTOs for every entity, I am using the AutoMapper library in my BL as it makes the mappings easy.

The UI of the page looks like this. I am using the GridView control to display the table with data, and the DataPager control to render links to other pages below my table.

The sample application uses GridView and DataPager from DotVVM Business Pack because they offer more features. However, if you use the controls from the open source framework, you can. They work the same way – just change <bp:GridView> to <dot:GridView>.

  1. <div class="toolbar">  
  2.     <dot:RouteLink RouteName="Admin_RegionDetail" class="dotvvm-bp-button">  
  3.         <bp:FAIcon Icon="Plus" /> New Region  
  4.     </dot:RouteLink>   
  5. </div>  
  6.   
  7. <bp:GridView DataSource="{value: Regions}">  
  8.     <bp:GridViewTextColumn ValueBinding="{value: Id}" HeaderText="Id" Width="50px" />  
  9.     <bp:GridViewTextColumn ValueBinding="{value: RegionDescription}" HeaderText="Description" />  
  10.   
  11.     <bp:GridViewTemplateColumn CssClass="icon">  
  12.         <dot:RouteLink RouteName="Admin_RegionDetail" Param-Id="{value: Id}">  
  13.             <bp:FAIcon Icon="Pencil" />  
  14.         </dot:RouteLink>  
  15.     </bp:GridViewTemplateColumn>  
  16.   
  17.     <bp:GridViewTemplateColumn CssClass="icon">  
  18.         <dot:LinkButton Click="{command: _root.Delete(Id)}">  
  19.             <PostBack.Handlers>  
  20.                 <dot:ConfirmPostBackHandler Message="Do you really want to delete the region?" />  
  21.             </PostBack.Handlers>  
  22.   
  23.             <bp:FAIcon Icon="Remove" />  
  24.         </dot:LinkButton>  
  25.     </bp:GridViewTemplateColumn>  
  26. </bp:GridView>  
  27.   
  28. <bp:DataPager DataSet="{value: Regions}" />  

List Page with GridView control
Figure 1: List Page with GridView control

Routes and Links

To generate a link to another page, I am using the RouteLink control. This control can generate URLs based on the route table: you can just specify the name of the route and the parameters (using the Param- prefix):

  1. <dot:RouteLink RouteName="Admin_RegionDetail" Param-Id="{value: Id}">…  

 

The route URL looks like this.

  1. admin/RegionDetail/{Id?}  

 

The value of the Id property is not required, so the New Region button is missing the Param-Id attribute.

Delete Confirmations

To prevent users from clicking the Delete button accidentally, I am using the ConfirmPostBackHandler. It will display a confirmation window before the postback for the Delete method is made, allowing the user to cancel the action.

  1. <dot:LinkButton Click="{command: _root.Delete(Id)}">  
  2.     <PostBack.Handlers>  
  3.         <dot:ConfirmPostBackHandler Message="Do you really want to delete the region?" /> </PostBack.Handlers>  
  4.     <bp:FAIcon Icon="Remove" />   
  5. </dot:LinkButton>  

This mechanism of postback handlers is extensible, so you can write custom postback handlers and render the dialog yourself so it fits the design of your application.

Detail Page

If you look at the RegionDetail.dothtml page, there is a simple form that can edit one region. I am using the same page for creating new regions, as well as to edit a region because regions are quite simple. However, some entities may require different forms for insert and edit.

The form is very simple as there is only one editable field,

  1. <div class="toolbar">  
  2.     <bp:Button Click="{command: Save()}" ButtonTagName="button">  
  3.         <bp:FAIcon Icon="Save" /> Save Changes </bp:Button>  
  4.     <bp:Button Click="{command: Cancel()}" ButtonTagName="button" Validation.Enabled="false">  
  5.         <bp:FAIcon Icon="Undo" /> Cancel </bp:Button>  
  6. </div>  
  7. <div class="form" DataContext="{value: CurrentItem}">  
  8.     <div class="form-field" Visible="{value: !_root.IsNew}"> <label>ID</label>  
  9.         <div> {{value: Id}} </div>  
  10.     </div>  
  11.     <div class="form-field" Validator.Value="{value: RegionDescription}"> <label>Description</label>  
  12.         <div>  
  13.             <bp:TextBox Text="{value: RegionDescription}" /> </div>  
  14.     </div>  
  15. </div>  

The form element has a binding expression on the DataContext property. It means that all bindings in the form will be evaluated on the CurrentItem property in the viewmodel, which contains the region object. Its type is RegionDTO, and it looks like this:

  1. public class RegionDTO: IEntity < int > {  
  2.     public int Id {  
  3.         get;  
  4.         set;  
  5.     }  
  6.     [Required]  
  7.     public string RegionDescription {  
  8.         get;  
  9.         set;  
  10.     }  
  11. }  

Notice the Required validation attribute (from System.ComponentModel.DataAnnotations namespace). DotVVM validates the ViewModel on every button click by default, so when the user tries to save the region with the empty description, he will get the validation error.

DotVVM also translates the validation attributes in JavaScript so the emptiness check will be made on the client side.

On the top of the page, there are Save and Cancel buttons. Notice that the Cancel button has Validation.Enabled set to false, because I do not want to validate the form when the user does not want to save the data.

The ViewModel of the page looks like this,

  1. public class RegionDetailViewModel: AdminViewModel {  
  2.     private readonly AdminRegionsFacade pageFacade;  
  3.     public RegionDetailViewModel(AdminRegionsFacade pageFacade) {  
  4.             this.pageFacade = pageFacade;  
  5.         }  
  6.         [FromRoute("Id")]  
  7.     public int CurrentItemId {  
  8.         get;  
  9.         private set;  
  10.     }  
  11.     public bool IsNew => CurrentItemId == 0;  
  12.     public RegionDTO CurrentItem {  
  13.         get;  
  14.         set;  
  15.     }  
  16.     public override Task PreRender() {  
  17.         if (!Context.IsPostBack) {  
  18.             if (CurrentItemId != 0) {  
  19.                 CurrentItem = pageFacade.GetDetail(CurrentItemId);  
  20.             } else {  
  21.                 CurrentItem = pageFacade.InitializeNew();  
  22.             }  
  23.         }  
  24.         return base.PreRender();  
  25.     }  
  26.     public void Save() {  
  27.         pageFacade.Save(CurrentItem);  
  28.         Context.RedirectToRoute("Admin_RegionList");  
  29.     }  
  30.     public void Cancel() {  
  31.         Context.RedirectToRoute("Admin_RegionList");  
  32.     }  
  33. }  

The CurrentItemId property is decorated with FromRoute attribute, which tells DotVVM to place the value of the route parameter named Id in this property.

The IsNew property calculates whether we edit or create a new record. If the Id parameter is not present in the route, the CurrentItemId will have the default value (zero).

Look how I hide the Id field in the form for new records. You can set Visible property on any HTML element or DotVVM control.

  1. <div class="form-field" Visible="{value: !_root.IsNew}">  

The CurrentItem property contains the RegionDTO instance which can be edited by the user. In the PreRender method, I either load this object using the facade or create a new instance with default values, also utilizing a facade.

The Save method takes the DTO and passes it back to the facade, which stores it in the database.

The validation on the Save button is enabled by default, so when the region description is empty, the Save method is not called, and the validators will appear.

Notice that the div element which contains the region description field has the Validator.Value attribute referencing the property that is validated.

  1. <div class="form-field" Validator.Value="{value: RegionDescription}">  

In my master page, I have set global options for validation.

  1. <body Validator.InvalidCssClass="has-error" Validator.SetToolTipText="true">  

You can set these attributes globally for the entire application as I did. Alternatively, you can set these properties on the div element itself, or on any of its parents. 

Validator.InvalidCssClass tells DotVVM to add the has-error CSS class to elements marked by Validator.Value when their property is invalid.

Validator.SetToolTipText adds the title attribute to these elements so that the user will see the error on mouse hover.

Validation errors
Figure 2: Validation errors

DotVVM also ships with the ValidationSummary control which can aggregate all errors from the ViewModel (or its part), and display them as a list, which can be styled using CSS.

Conclusion

I have tried to show how a trivial CRUD can be implemented in DotVVM. In the next part of this series, I will show how to make a generic CRUD ViewModel that will save quite a lot of code while providing some extensibility points for more complicated scenarios.

Resources


Similar Articles