Show / Hide Table of Contents

RDO.WPF Overview

Built on top of RDO.Data, RDO.WPF is a presentation layer which can greatly help developers to build WPF desktop applications:

image

MVP Architecture

RDO.WPF is implemented as Model-View-Presenter (MVP) to improve the separation of concerns in presentation logic. The following is the architecture of RDO.WPF MVP:

image

  • 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.

How to Use

Using RDO.WPF can be as simple as:

  • Add nuget package DevZest.Data.WPF into your WPF project;
  • Add a DataView control into your UI;
  • Add your presenter class derived from DataPresenter<T>, implement the abstract BuildTemplate method, which takes a DataPresenter.TemplateBuilder parameter.
  • Call Show method of your presenter class to show data to the view.

Here is an example of presenter class implementation:

  • C#
  • VB.Net
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;
            }
        }
    }
}
Imports DevZest.Data
Imports DevZest.Data.Presenters
Imports DevZest.Data.Views

Partial Class SalesOrderWindow
    Private Class DetailPresenter
        Inherits DataPresenter(Of SalesOrderInfoDetail)
        Implements ForeignKeyBox.ILookupService
        Implements DataView.IPasteAppendService
        Public Sub New(ownerWindow As Window)
            _ownerWindow = ownerWindow
        End Sub

        Private ReadOnly _ownerWindow As Window

        Protected Overrides Sub BuildTemplate(builder As TemplateBuilder)
            Dim e = Entity
            Dim product = e.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, Me.BindToGridHeader()) _
                .AddBinding(1, 0, product.ProductNumber.BindToColumnHeader("Product No.")) _
                .AddBinding(2, 0, product.Name.BindToColumnHeader("Product")) _
                .AddBinding(3, 0, e.UnitPrice.BindToColumnHeader("Unit Price")) _
                .AddBinding(4, 0, e.UnitPriceDiscount.BindToColumnHeader("Discount")) _
                .AddBinding(5, 0, e.OrderQty.BindToColumnHeader("Qty")) _
                .AddBinding(6, 0, e.LineTotal.BindToColumnHeader("Total")) _
                .AddBinding(0, 1, e.BindTo(Of RowHeader)()) _
                .AddBinding(1, 1, e.FK_Product.BindToForeignKeyBox(product, AddressOf GetProductNumber).MergeIntoGridCell(product.ProductNumber.BindToTextBlock()).WithSerializableColumns(e.ProductID, product.ProductNumber)) _
                .AddBinding(2, 1, product.Name.BindToTextBlock().AddToGridCell().WithSerializableColumns(product.Name)) _
                .AddBinding(3, 1, e.UnitPrice.BindToTextBox().MergeIntoGridCell()) _
                .AddBinding(4, 1, e.UnitPriceDiscount.BindToTextBox(New PercentageConverter()).MergeIntoGridCell(e.UnitPriceDiscount.BindToTextBlock("{0:P}"))) _
                .AddBinding(5, 1, e.OrderQty.BindToTextBox().MergeIntoGridCell()) _
                .AddBinding(6, 1, e.LineTotal.BindToTextBlock("{0:C}").AddToGridCell().WithSerializableColumns(e.LineTotal))
        End Sub

        Private Shared Function GetProductNumber(valueBag As ColumnValueBag, productKey As Product.PK, productLookup As Product.Lookup) As String
            Return valueBag.GetValue(productLookup.ProductNumber)
        End Function

        Private Function CanLookup(foreignKey As CandidateKey) As Boolean Implements ForeignKeyBox.ILookupService.CanLookup
            If foreignKey Is Entity.FK_Product Then
                Return True
            Else
                Return False
            End If
        End Function

        Private Sub BeginLookup(foreignKeyBox As ForeignKeyBox) Implements ForeignKeyBox.ILookupService.BeginLookup
            If foreignKeyBox.ForeignKey Is Entity.FK_Product Then
                Dim dialogWindow = New ProductLookupWindow()
                dialogWindow.Show(_ownerWindow, foreignKeyBox, CurrentRow.GetValue(Entity.ProductID))
            Else
                Throw New NotSupportedException()
            End If
        End Sub

        Protected Overrides Function ConfirmDelete() As Boolean
            Return MessageBox.Show(String.Format("Are you sure you want to delete selected {0} rows?", SelectedRows.Count), "Delete", MessageBoxButton.YesNo) = MessageBoxResult.Yes
        End Function

        Private Function Verify(data As IReadOnlyList(Of ColumnValueBag)) As Boolean Implements DataView.IPasteAppendService.Verify
            Dim foreignKeys = DevZest.Data.DataSet(Of Product.Ref).Create()
            For i = 0 To data.Count
                Dim valueBag = data(i)
                Dim productId = If(valueBag.ContainsKey(Entity.ProductID), valueBag(Entity.ProductID), Nothing)
                foreignKeys.AddRow(Sub(e, dataRow) e.ProductID.SetValue(dataRow, productId))
            Next

            Dim lookup As DataSet(Of Product.Lookup) = Nothing
            If Not App.Execute(Function(db, ct) db.LookupAsync(foreignKeys, ct), Window.GetWindow(View), lookup) Then
                Return False
            End If

            Debug.Assert(lookup.Count = data.Count)
            Dim product = Entity.Product
            For i = 0 To lookup.Count
                data(i).SetValue(product.Name, lookup.Entity.Name(i))
                data(i).SetValue(product.ProductNumber, lookup.Entity.ProductNumber(i))
            Next
            Return True
        End Function
    End Class
End Class

The above code produce the following data grid UI, with foreign key lookup and paste append from clipboard implemented:

image

  • Improve this Doc
Back to top Copyright © Weifen Luo | DevZest