Presentation Layer, the New Way
Separation of the graphical user interface from the business logic or back-end logic (the data model), is still a challenge task. Frameworks such as MVVM introduces a new layer (the view model), trying to handle most if not all of the view's display logic.
Presentation logic is complex - it's responsible for overcoming the gap between computer and human being, which is a big (maybe biggest) challenge in computer science. To encapsulate presentation logic into a separate layer, we need to plan carefully at the very beginning. Unfortunately, existing MVVM implementations, among other similar frameworks, are built as an afterthought.
The Anti-Patterns
The POCO Obsession, Again
When model is arbitrary object (POCO), hiding the model from the view is simply not possible. The presentation layer can do little about it, in many cases, it just expose the model object via aggregation, without any value-added.
On the other hand, data model cannot be 100% POCO. For example, INotifyPropertyChanged
interface is mandatory for most data models, and IDataErrorInfo
interface is required if you want to bind to custom validation error. Since these interfaces must be implemented by all data models, they must be dead simple.
In the end, your presentation layer can do little with the data model.
Complex Control
Complex control, such as DataGrid
, has very complex presentation logics. Since these controls are built without existing presentation layer, these logics are naturally encapsulated into the control itself - the view. This puts the presentation layer into an embarrassed situation: for simple control without complex view state such as TextBlock
, it has little job to do; for complex control such as DataGrid
, the control has done the job.
In the end, your presentation layer can do little with the view too.
Put it together, if the presentation layer is an afterthought, there is little room left for implementation, especially at abstraction level.
The New Way
Thanks to RDO.Data, which provides a rich set of data objects and the separation of model of data, we now have the foundation to implement a comprehensive Model-View-Presenter (MVP) pattern. The following is the architecture of RDO.WPF MVP:
- The model contains the data values and data logic such as computation and validation in a DataSet<T> object. The DataSet<T> object contains collection of DataRow objects and Column objects, similar as two dimensional array. The model provides events to notify data changes, it does not aware the existence of the presenter at all.
- The view contains UI components which directly interacts with user. These UI components are designed as dumb as possible, all presentation logic are implemented in the presenter. Despite the container UI components such as DataView, BlockView and RowView, or controls depending on presentation logic implemented in presenter (such as ColumnHeader), most UI elements do not aware the existence of the presenter at all.
- The presenter is the core to tie model and view together, it implements the following presentation logic:
- Selection, filtering and hierarchical grouping.
- UI elements life time management and data binding.
- Editing and validation.
- Layout and UI virtualization.
Since presenting collection of data is extensively and exclusively supported by the presenter, all the complex controls (controls derived from System.Windows.Controls.ItemsControl
such as ListBox
and DataGrid
) are not necessary any more. By using RDO.WPF, your application only need to deal with simple controls such as TextBlock
and TextBox
, via data binding, in an unified way.
Simply derive you data presenter from DataPresenter<T> class, which contains the presentation logic implementation, and put a DataView into your view, you got all the presentation logic such as filtering, sorting, grouping, selection, data binding, editing and layout immediately, without using any complex control. For example, the following code:
using DevZest.Data.Presenters;
using DevZest.Data.Views;
using DevZest.Data;
using System.Windows;
using System;
using System.Windows.Controls;
using System.Collections.Generic;
using System.Diagnostics;
namespace DevZest.Samples.AdventureWorksLT
{
partial class SalesOrderWindow
{
private class DetailPresenter : DataPresenter<SalesOrderInfoDetail>, ForeignKeyBox.ILookupService, DataView.IPasteAppendService
{
public DetailPresenter(Window ownerWindow)
{
_ownerWindow = ownerWindow;
}
private readonly Window _ownerWindow;
protected override void BuildTemplate(TemplateBuilder builder)
{
var product = _.Product;
builder.GridRows("Auto", "20")
.GridColumns("20", "*", "*", "Auto", "Auto", "Auto", "Auto")
.WithFrozenTop(1)
.GridLineX(new GridPoint(0, 2), 7)
.GridLineY(new GridPoint(2, 1), 1).GridLineY(new GridPoint(3, 1), 1).GridLineY(new GridPoint(4, 1), 1)
.GridLineY(new GridPoint(5, 1), 1).GridLineY(new GridPoint(6, 1), 1).GridLineY(new GridPoint(7, 1), 1)
.Layout(Orientation.Vertical)
.WithVirtualRowPlacement(VirtualRowPlacement.Tail)
.AllowDelete()
.AddBinding(0, 0, this.BindToGridHeader())
.AddBinding(1, 0, product.ProductNumber.BindToColumnHeader("Product No."))
.AddBinding(2, 0, product.Name.BindToColumnHeader("Product"))
.AddBinding(3, 0, _.UnitPrice.BindToColumnHeader("Unit Price"))
.AddBinding(4, 0, _.UnitPriceDiscount.BindToColumnHeader("Discount"))
.AddBinding(5, 0, _.OrderQty.BindToColumnHeader("Qty"))
.AddBinding(6, 0, _.LineTotal.BindToColumnHeader("Total"))
.AddBinding(0, 1, _.BindTo<RowHeader>())
.AddBinding(1, 1, _.FK_Product.BindToForeignKeyBox(product, GetProductNumber).MergeIntoGridCell(product.ProductNumber.BindToTextBlock()).WithSerializableColumns(_.ProductID, product.ProductNumber))
.AddBinding(2, 1, product.Name.BindToTextBlock().AddToGridCell().WithSerializableColumns(product.Name))
.AddBinding(3, 1, _.UnitPrice.BindToTextBox().MergeIntoGridCell())
.AddBinding(4, 1, _.UnitPriceDiscount.BindToTextBox(new PercentageConverter()).MergeIntoGridCell(_.UnitPriceDiscount.BindToTextBlock("{0:P}")))
.AddBinding(5, 1, _.OrderQty.BindToTextBox().MergeIntoGridCell())
.AddBinding(6, 1, _.LineTotal.BindToTextBlock("{0:C}").AddToGridCell().WithSerializableColumns(_.LineTotal));
}
private static string GetProductNumber(ColumnValueBag valueBag, Product.PK productKey, Product.Lookup productLookup)
{
return valueBag.GetValue(productLookup.ProductNumber);
}
bool ForeignKeyBox.ILookupService.CanLookup(CandidateKey foreignKey)
{
if (foreignKey == _.FK_Product)
return true;
else
return false;
}
void ForeignKeyBox.ILookupService.BeginLookup(ForeignKeyBox foreignKeyBox)
{
if (foreignKeyBox.ForeignKey == _.FK_Product)
{
var dialogWindow = new ProductLookupWindow();
dialogWindow.Show(_ownerWindow, foreignKeyBox, CurrentRow.GetValue(_.ProductID));
}
else
throw new NotSupportedException();
}
protected override bool ConfirmDelete()
{
return MessageBox.Show(string.Format("Are you sure you want to delete selected {0} rows?", SelectedRows.Count), "Delete", MessageBoxButton.YesNo) == MessageBoxResult.Yes;
}
bool DataView.IPasteAppendService.Verify(IReadOnlyList<ColumnValueBag> data)
{
var foreignKeys = DataSet<Product.Ref>.Create();
for (int i = 0; i < data.Count; i++)
{
var valueBag = data[i];
var productId = valueBag.ContainsKey(_.ProductID) ? valueBag[_.ProductID] : null;
foreignKeys.AddRow((_, dataRow) =>
{
_.ProductID.SetValue(dataRow, productId);
});
}
if (!App.Execute((db, ct) => db.LookupAsync(foreignKeys, ct), Window.GetWindow(View), out var lookup))
return false;
Debug.Assert(lookup.Count == data.Count);
var product = _.Product;
for (int i = 0; i < lookup.Count; i++)
{
data[i].SetValue(product.Name, lookup._.Name[i]);
data[i].SetValue(product.ProductNumber, lookup._.ProductNumber[i]);
}
return true;
}
}
}
}
Will produce the following editable data grid UI, with implementation of foreign key lookup and clipboard support:
In the end, you have ALL of your presentation logic in 100% strongly typed, highly reusable clean code.
The data presentation described previously has been implemented in Windows Presentation Foundation (WPF) desktop development. Unfortunately, the whole thing cannot be ported to the world of web development, at this moment.
Web Development, Now and Future
Web development is further divided into server-side and client-side development. ASP.Net Core is the answer from Microsoft to web development:
Name | Description |
---|---|
Razor Pages | Server-side page-focused framework. |
MVC | Server-side traditional Model-View-Controller framework. |
Blazor | Client-side web UI framework with .NET |
SPA | Client-side frameworks support: Angular, React, React with Redux, JavaScript Services |
Razor Pages and MVC are very similar in terms of data presentation. The key difference between Razor pages and MVC is that the model and controller code is also included within the Razor Page itself[1]. They are perfect for simple pages that are read-only or do basic data input. Since web application is stateless and there is no UI on the server, the complex UI logic cannot and should not be handled on the server.
Blazor, on the other hand, relies on WebAssembly (abbreviated wasm), with .Net runtime hosted in the browser. Despite it's still in beta phase, the real challenge is DOM: DOM is not designed for interactive UI, so the complex UI logic cannot be handled, or at least elegantly, either.
At this moment, RDO.Net provides DataSet support for Razor Pages server-side development, as NuGet package DevZest.Data.AspNetCore:
- DataSet JSON serialization/deserialization;
- DataSet model binding;
- DataSet tag helpers;
- DataSet validation.
In the future, when WebAssembly becomes mature and popular, with an additional layer of non-DOM based (canvas based, for example) simple UI components, the data presentation described previously can be ported to client-side web development.