Important Note
Friends, I'll be happy to hear any feedback from you in the comments section. Thanks
Introduction
When a software team works on building an application, it is necessary to maintain a balance between enabling different people to work as independently of each other as possible and making the fruits of their labor work together seamlessly. Because of the need for this balance, almost every successful UI architecture splits the application into Widgets.
Widget is a primary UI unit that can be developed and tested independently of the rest of the Widgets and might have its own value for the users even outside of the whole application.
WPFWidgetizer framework based architecture presented in article will address the need for creating a flexible UI infrastructure consisting of Widgets seamlessly working together. The techniques presented here have been tested and found to be working in numerous successful projects.
Please, note that WPFWidgetizer framework is expected to continue undergo changes and further development.
Reading and working through this article will save millions of dollars for your project: by building a flexible infrastructure you will be able to concentrate on creating the individual widgets that you need for the business, instead of struggling to make them work together. Moreover, learning these techniques will allow you to avoid making wrong architectural decisions (architectural anti-patterns, which unfortunately plague many WPF projects).
We call this approach building "Extensible and Flexible" applications because it allows to create applications that consist of widgets that can be easily added, removed or re-configured. These widgets communicate with each other using Prism's EventAggregator
messages.
In architecture (as opposed to implementation) the major unit of the application is a widget, not individual controls, so we are going to concentrate on describing how to build the application at the widget level. Of course, we will also be using some simple controls as finer building blocks, but the focus will be on the widgets.
We almost avoid using any third party components within our application, with the exception of Microsoft's Prism and Microsoft's Expression Blend SDK.
We use Prism as an Inversion of Control (IoC) container and also for its event aggregator messaging functionality.
Expression Blend SDK is being used to provide the plumbing between XAML and C# code. Unlike Expression Blend, Expression Blend SDK is free to use and redistribute.
All the required dll files for Prism and Exression Blend SDK are provided with our samples, so you do not have to download anything extra.
Most of the techniques described in this article can be applied to building any UI client application, not necessarily a WPF or even .NET one. All of our samples, however, are built using WPF.
We do not discuss here implementing the database and service layers for the applicaion. We do, however, show how to implement a mock service layer that would return mock data to the client. We also present a mechanism for swapping this mock service layer with a real one with almost no change for the rest of the application.
This article is not for WPF beginners - I assume some knowledge of WPF, including MVVM pattern, dependency, attached and notifiable properties, bindings, routed events, XAML.
Article Overview
Contrary to my original intention, the material I want to cover proved to be too large for one article - it will have to be divided into two installments. Here are the topics covered in this installment:
Here are the major topics we are going to cover in this article:
- Refresher on using Prism as IoC Container and Event Aggregation.
- Widget - Widgets Assembly Pattern - this is the core pattern of the article.
- Discussion of the Code Structure.
- Exploration of the View Model and UI code as well as data fetching functionality via the Single Widget Sample.
- Exploration of Widget UI action events, inter-Widget messaging and Widget Assembly functionality via the Simple Widget Assembly Sample.
And here are the major topics that will be covered in the sequel:
- Widgets that contain Widget Assembly with sub-widgets within them.
- Scope based hierarchical inter-Widget communications.
- Navigation between different Widget Assemblies.
- Element Factory Pattern.
- View Model Hierarchy Pattern.
Refresher on Inversion of Control (IoC) and Event Aggregation using MS Prism Framework
I recommend that even those who know the IoC and Event Aggregation well, should go over this refresher, since it describes how we use these concepts further in the article and gives some ideas about the code structure.
Inversion of Control (IoC)
What IoC is Used For
IoC allows to swap parts of the application that implement certain interfaces with completely different implementations of the same iterfaces with little or no code change. Most of the time IoC is used for providing temporary or test plugins for some (usually non-visual) parts of the application. For example, in many projects I was using a mock data plugin in order to build an application before the backend/services are ready for it. Even if the backend is ready for usage, you might still want to simpulate your own mock data to test that the application behaves well under some rare conditions or simply to make the data return faster.
There are many various types of IoC containers including MEF and Unity. Throughout this article we are going to be using MEF container that is part of Microsoft's Prism software.
IoC Overuse Anti-Pattern
Many application architects might disagree with me, but I am convinced that in many projects the IoC is greatly overused. Unfortunately, MEF and other IoC containers facilitate chain propagation of plugins, meaning - if a component is implemented as a MEF plugin, it is easier to implement the classes that contains this component as a MEF plugin also.
Overusing of plugin architecture leads to confusing code, unclear exceptions and difficulty in debugging while it does not make the application any more flexible.
Consider yourself cautioned, use plugins only for those parts whose implementation you really might want to swap for a different one at some point during the project!
IoC Prism Sample
We are using Prism 5.0 for .NET 4.5 freely available at Microsoft Prism Download.
You do not have to download and install the packages from the above URL, since we provide all necessary dll files with our samples.
The sample is located under IoCPluginSample.sln solution within Samples/IoCPluginSample folder.
Before opening the project, you should unblock the Prism dll files (they are likely to be blocked since you downloaded them from the internet). To unblock them, go to ExternalPackages/Prism folder which contains them, right click on each file, choose Properties option and click Unblock button.
The purpose of this sample is to show how easy it is to swap between different implementation of the same interface by using Prism with MEF.
Interface INumberChurner
is defined under GenericInfrastructure
project:
[InheritedExport(typeof(INumberChurner))]
public interface INumberChurner
{
int GetInt();
}
It has only one method GetInt()
that returns an integer. Two different projects SampleMockup1
and SampleMockup2
are located under TestAndMockupUtils
folder. They provide two different implementations of INumberChurner
- classes Number1Churner
and Number2Churner
.
GetInt()
method of Number1Churner
always returns 1 while GetInt()
method of Number2Churner
always returns 2.
InheritedExport(typeof(INumberChurner))
MEF attributed above INumberChurner
declaration, ensures that classes that implement INumberChurner
are MEF exports with the same MEF id provided by typeof(INumberChurner)
or, in other words that can be swapped by MEF.
MEF allows you to get implementation by MEF id from a MEF container. Because of this, it is important to have an easy way of getting a reference to MEF container all over the application. To provide such reference, we created a static class TheAppIoCContainer
under GenericInfrastructure
project. This class contains TheCompositionContainer
property that refers to the MEF container throughout the application. This property is set to be a reference to the MEF container by AppBootstrapperBase
class:
public abstract class AppBootstrapperBase : MefBootstrapper
{
protected override System.ComponentModel.Composition.Hosting.CompositionContainer CreateContainer()
{
CompositionContainer theContainer = base.CreateContainer();
TheAppIoCContainer.TheCompositionContainer = theContainer;
return theContainer;
}
}
Now, let us switch our attention to IoCPluginSample
project (the main project of the application).
TheBootstrapper
class under IoCPluginSample
project inherits from GenericInfrastructure.AppBootstrapperBase
class:
public class TheBootstrapper : AppBootstrapperBase
{
protected override void InitializeShell()
{
base.InitializeShell();
Application.Current.MainWindow = this.Shell as Window;
Application.Current.MainWindow.Show();
}
protected override DependencyObject CreateShell()
{
return Container.GetExportedValue<mainwindow>();
}
protected override void ConfigureAggregateCatalog()
{
base.ConfigureAggregateCatalog();
AggregateCatalog.Catalogs.Add(new AssemblyCatalog(this.GetType().Assembly));
AggregateCatalog.Catalogs.Add(new AssemblyCatalog(typeof(Number1Churner).Assembly));
}
}
</mainwindow>
Method ConfigureAggregateCatalog
is the one that we use for swapping implementations between that of SampleMockups1
and of SampleMockup2
projects - in order to choose one implementation over the other, all you need to do is to leave the corresponding AggregateCatalog.Catalogs.Add(...)
line uncommented and comment out the other line.
Class MainWindow
defines a integer property TheNumber
which is set by the corresponding NumberChurner
:
[Export]
public partial class MainWindow : Window
{
public MainWindow()
{
INumberChurner numberChurner =
TheAppIoCContainer.TheCompositionContainer.GetExportedValue<inumberchurner>();
TheNumber = numberChurner.GetInt();
InitializeComponent();
}
public int TheNumber
{
get;
private set;
}
}
</inumberchurner>
MainWindow.xaml displays TheNumber
property of the MainWindow
class:
<Grid>
<TextBlock Text="{Binding Path=TheNumber,
RelativeSource={RelativeSource AncestorType=Window}}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="80"/>
</Grid>
When we run the application, it will show number 1 in the middle of the white window.
If, however, we change the ConfigureAggregateCatalog()
method of TheBootstrapper
class to have second AggregateCatalog.Catalogs.Add(...)
line uncommented:
AggregateCatalog.Catalogs.Add(new AssemblyCatalog(typeof(Number2Churner).Assembly));
The application will show number 2.
Note, that if we have more methods within our iterface, or different interfaces, and various assemblies providing different implementation for those interfaces, we would still be able to change between these different implementations by changing the comments on one line within TheBootstrapper.ConfigureAggregateCatalog()
method.
Before we continue, I suggest a small improvement to the code above. TheBootstrapper
class will have to be part of the start up project of every sample (we won't be able to reuse it). It makes sense, to factor out most of its functionality into a superclass. In particular, we can factor out functions InitializeShell()
and CreateShell()
. We already have our own superclass AppBootstrapperBase
defined under GenericInfrastructure
project. We do not want to use this class, for defining the functions InitializeShell()
and CreateShell()
, however, because it will require adding a dependency on some WPF specific dlls for the GenericInfrastructure
project and we want to be able to use its functionality in purely non-visual projects. Because of that, we add a project IoCPluginUtils
and define there a class AppBootstrapper
derived from AppBootstrapperBase
. AppBootstrapper
will provide all the plumbing functionality, so that the individual sample bootstrappers will only have to define ConfigureAggregateCatalog()
method:
public class AppBootstrapper<T> : AppBootstrapperBase where T : Window
{
protected override void InitializeShell()
{
base.InitializeShell();
Application.Current.MainWindow = this.Shell as Window;
Application.Current.MainWindow.Show();
}
protected override DependencyObject CreateShell()
{
return Container.GetExportedValue<T>();
}
protected override void ConfigureAggregateCatalog()
{
base.ConfigureAggregateCatalog();
AggregateCatalog.Catalogs.Add(new AssemblyCatalog(typeof(T).Assembly));
}
}
And TheBootstrapper
class of the main project becomes mere:
public class TheBootstrapper : AppBootstrapper<MainWindow>
{
protected override void ConfigureAggregateCatalog()
{
base.ConfigureAggregateCatalog();
AggregateCatalog.Catalogs.Add(new AssemblyCatalog(typeof(Number1Churner).Assembly));
}
}
This is all we need from the IoC capabilities of Prism.
Event Aggregation
What Event Aggregation is Used for
Event aggregation allows separate parts of the application communicate with each other without having any knowledge of each other - thus enabling better separation of concerns. One of the parts publishes a message via an EventAggregator
while the other part(s) can register a handler for that type of the message, so that when the message arrives, the handler fires.
Most WPF frameworks, provide their own Event Aggregation functionalty. Here we shall be using Prism event aggregator.
Event Aggregation Overuse Anti-Pattern
Just like the IoC, the Event Aggregation should be used with caution. When people adopt event aggregation they tend to overuse it for almost every communication within the application. This leads to compicated spaghetti code difficult to extend, debug and understand.
I suggest using Event Aggregation only for communications between larger entities within the application, in particular for communications between the Widgets, while the communication within each Widget should be handled differently. We'll speak much more about it below.
Event Aggregation Sample
The Prism Event Aggregation sample is located under EventAggregatorSample.sln solution within EventAggregatorSample folder. The main project bears the same name EventAggregatorSample
.
Even though, I recommended above to use Event Aggregation only at the widget level or higher, here, for simplicity sake, I'll demonstrate using the event aggregation for communications between different controls defined within the same window; inter-Widget communication will be explained further down in this article.
Try running the project, you will the following:
If you press "Publish Time Stamp" button, nothing is going to happen because the subscription to the event has not taken place yet.
If you check "Subscribe/Unsubscribe" checkbox, however, and try pushing the button again, you will see time stamps published on the right hand side of the window:
Now, let us describe the code.
Under project GenericInfrastructure
I created a class EventAggregatorSingleton
. It is a wrapper around Prism's Event Aggregator functionality. It is static, so that it simplifies access to the event aggregator (I've never heard of using more than one Event Aggregator within an application, so we are not losing any genericity). This class contains several very simple methods for publishing messages, subscribing to messages and unsubscribing from messages:
public static class EventAggregatorSingleton
{
static IEventAggregator _eventAggregator = null;
private static IEventAggregator TheEventAggregator
{
get
{
if (_eventAggregator == null)
{
if (TheAppIoCContainer.TheCompositionContainer == null)
return null;
_eventAggregator =
TheAppIoCContainer.TheCompositionContainer.GetExportedValue<IEventAggregator>();
}
return _eventAggregator;
}
}
private static PubSubEvent<T> GetPrismAggregationEvent<T>()
{
PubSubEvent<T> prismAggregationEvent =
TheEventAggregator.GetEvent<PubSubEvent<T>>();
return prismAggregationEvent;
}
public static void Publish<T>(T eventAggregatorMessage)
{
GetPrismAggregationEvent<T>().Publish(eventAggregatorMessage);
}
public static PrismUnsubscriber Subscribe<T>
(
Action<T> action,
Predicate<T> filter = null,
ThreadOption threadOption = ThreadOption.PublisherThread,
bool keepSubscriberReferenceAlive = true
)
{
PubSubEvent<T> prismAggregationEvent = GetPrismAggregationEvent<T>();
SubscriptionToken subscriptionToken =
prismAggregationEvent.Subscribe
(
action,
threadOption,
keepSubscriberReferenceAlive,
filter
);
PrismUnsubscriber result =
new PrismUnsubscriber(prismAggregationEvent, subscriptionToken);
return result;
}
public static void Unsubscribe<T>(this SubscriptionToken token)
{
PubSubEvent<T> prismAggregationEvent = GetPrismAggregationEvent<T>();
prismAggregationEvent.Unsubscribe(token);
}
public static void Unsubscribe<T>(Action<T> action)
{
PubSubEvent<T> prismAggregationEvent = GetPrismAggregationEvent<T>();
prismAggregationEvent.Unsubscribe(action);
}
}
You can see, that publishing/subscribing to Prism events is done by the message's C# type. If you subscribe to a certain message type, you'll be receiving all the published messages of that C# type. There is, however, a filtering parameter to the Subscribe(...)
function that allows to refine the subscription only to messages that satisfy the filtering condition.
Most of the sample specific functionality is located under the main project of the application: EventAggregatorSample
project within MainWindow.xaml and MainWindow.xaml.cs files. This project is referencing two dll files from MS Expression Blend SDK - Microsoft.Expression.Interactions.dll and System.Windows.Interactivity.dll. They are provided under ExternalPackages/ExpressionBlendSDK folder. You need to unblock these files in the same way as you have unblocked the prism dlls. BTW, these files just like the whole MS Expression Blend SDK (unlike Expression Blend itself) are free to use and re-distribute.
We use functionality from the MS Expression Blend SDKs in order to call C# functions from within XAML. To learn more about MS Expression Blend SDK, you can read e.g. parts of MVVM Pattern Made Simple.
Let us first take a look at MainWindow.xaml.cs file. It defines TheDateTimeCollection
as an ObservableCollection
. This collection is being populated by the subscription callback provided by OnTypeStampMessageArrived(DateTime timeStamp)
method.
PublishTimeStamp()
method publishes the current time stamp via the event aggregator.
IsSubscribed
boolean property allows to toggle between subscribed and unsubscribed states. It invokes subscription when the propery changes to true
and unsubscription when it changes to false
.
Here is the code for MainWindow.xaml.cs file:
[Export]
public partial class MainWindow : Window, INotifyPropertyChanged
{
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
protected void OnPropChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public MainWindow()
{
TheDateTimeCollection = new ObservableCollection<DateTime>();
InitializeComponent();
}
public ObservableCollection<DateTime> TheDateTimeCollection { get; private set; }
public void PublishTimeStamp()
{
DateTime timestampToPublish = DateTime.Now;
EventAggregatorSingleton.Publish<DateTime>(timestampToPublish);
}
PrismUnsubscriber _subscriptionToken = null;
bool _isSubscribed = false;
public bool IsSubscribed
{
get
{
return _isSubscribed;
}
set
{
if (_isSubscribed == value)
return;
_isSubscribed = value;
if (_isSubscribed)
{
_subscriptionToken = EventAggregatorSingleton.Subscribe<DateTime>(OnTimeStampMessageArrived);
}
else
{
if (_subscriptionToken != null)
{
_subscriptionToken.Unsubscribe();
_subscriptionToken = null;
}
}
OnPropChanged("IsSubscribed");
}
}
private void OnTimeStampMessageArrived(DateTime timeStamp)
{
TheDateTimeCollection.Add(timeStamp);
}
}
MainWindow.xaml file creates UI elements for using the functionality defined in MainWindow.xaml.cs file:
<Window x:Class="EventAggregatorSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
-->
<Button Width="120"
Height="25"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Content="Publish Time Stamp">
<i:Interaction.Triggers>
-->
<i:EventTrigger EventName="Click">
<ei:CallMethodAction MethodName="PublishTimeStamp"
TargetObject="{Binding RelativeSource={RelativeSource AncestorType=Window}}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
<StackPanel Grid.Column="1"
Margin="10">
<TextBlock Text="Display Published Timestamps" />
-->
<ListView Margin="0,20,0,20"
ItemsSource="{Binding Path=TheDateTimeCollection,
RelativeSource={RelativeSource AncestorType=Window}}"/>
</StackPanel>
</Grid>
-->
<CheckBox Grid.Row="1"
Content="Subscribe/Unsubscribe"
Margin="10,5,10,30"
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsChecked="{Binding Path=IsSubscribed, RelativeSource={RelativeSource AncestorType=Window}}"/>
</Grid>
</Window>
Widgets and Widgets Assembly Pattern
Widgets
Each Widget has its own View Model and its own wiring for getting the data from the backend/service layers. The widgets should also provide a mechanism for communicating with other widgets, preferably without knowing the other widget's types - so the Event Aggregation concept is ideal for communicating between different widgets. Widgets in WPFWidgetizer are descendants of WPF ContentControl
class. Each widget usually contains some finer level WPF Control
(s).
Widgets Assembly Pattern
In order for the several widgets to display and work together within an application, they should be assembled together into some larger entities. Often, such entities are called 'Views' (not to be confused with the Views of MVVM pattern). In this article, however, precisely in order to avoid confusion with the Views of the MVVM pattern we are going to call them "Widget Assemblies".
The purpose of a Widget Assembly is to define the mutual location and communications between the Widgets within it.
A lot of times Widget Assemblies are implemented in C#/XAML. They can be defined as usual WPF controls containing the widgets. Widget Assembly implementation can even include a View Model that would contain the View Models of the individual widgets and coordinate their behaviors. This approach, however, has a number of shortcomings:
- It increases complexity of the application. Each Widget Assembly will result in XAML/C# View files and also in a View Model file.
- It reduces the flexibility/configurability of the application - since C# functionality is usually difficult to imitate by configuration files.
- It reduces the uniformity of the application - C# is a very powerful language - using it for communications between the widgets might result in a wide range of different solutions by different developers on the project.
In order to improve the architecture and the speed of the development, the Widget Assembly functionality should be made as generic as possible. The solution proposed in this article has only one class for Widget Assembly, but multiple styles that specify the mutual location of the widgets. Using 3rd party widget docking functionality e.g. from Teleric or DevExpress, one can futher improve the genericity of the solution by having the Widget Assemblies defined by configuration files that can be modified at run time. This approach (even though it is beyond the scope of this article) would allow creating dynamic Widget Assemblies that can be put together by the users. This will result in unparallel flexibility enabling creating the Views almost on the fly by combining various Widgets together.
Code Structure
Here we describe the location of the projects for WPFWidgetizer framework and the samples and the relationships between them.
All the samples below show how to create the functionality for displaying various information about an imaginary book store, so the code can be sub-divided into generic and book store specific functionality.
The bird's eye view of the file structure of the WPFWidgetizer code is given by the following image:
"ExternalPackages" folder contains the dll files for the 3rd party components that we use: Prism and Expression Blend SDK.
The "GenericFramework" folder, contains generic functionality that can be used not only for building a book store application, but any application. This functionality can be distributed as dll files corresponding to the individual projects within "GenericFramework" folder.
Here is a brief description of all the "GenericFramework" projects (ordered from more generic to more specific):
GenericInfrastructure
is a project containing very generic non-visual utilities that can be used in any project and are not aware of Widget View Models or Widgets. It contains some basic utility wrappers around Prism's Event Aggregation and IoC functionality. It can be extened, e.g. to contain some generic extension methods for string manipulations etc.
UIControls
project contains generic UI Controls and UI styles that are not aware of widgets or Widget View Models.
IoCPluginUtils
contain only a generic bootstrapper class AppBootstrapper
that has been described above.
RecordVMs
project contains a generic non-visual code and base classes corresponding to the individual rows coming from the database/service layer (we assume that the data coming from the database is a collection of such rows).
WidgetVM
project provides the generic base and utility non-visual classes for the Widgets' View Models.
WidgetsAndAssemblies
project provides base and utility UI classes and styles for the widgets themselves.
"BookStoreSpecificFramework" folder contains projects specific to our book store related screen development:
BookStoreInfrastructure
contains book store specific enumerations and non-visual helper classes.
BookStoreRecordVMs
contains View Model classes for representing row data coming from the database or the service layer.
BookStoreWidgetVMs
contain book store specific Widgets' View Models.
BookStoreWidgets
contain UI classes and Styles for the book store related widgets.
"TestAndMockupUtils" folder contains projects facilitating tests and mockups. E.g. it has project MockupServiceLayer
that mocks up the data coming from database/service layer.
"Samples" folder contains solutions and main projects for various samples (two of them IoCPluginSample
and EventAggregatorSample
were described above).
Here is a diagram of project dependencies (the arrows are pointing to the dependency target project):
On the picture above and also in the discussions below, for the sake of brevity, I omit the namespace prefix to the project name e.g. the full project name is WPFWidgetizer.BookStoreSpecificFramework.BookStoreInfrastructure
, while on the picture it is shown simply as BookStoreInfrastructure
.
Note that projects under BookStore Specific Framework (on the right) depend on the corresponding projects from within Generic Framework (on the left).
Every project has a dependency on GenericInfrastructure
project and MainApplication
depends on all the rest of the projects (even though we do not show all the arrows coming out of GenericInfrastructure
and ending at MainApplication
- otherwise the picture will become a mess).
There is no project called MainApplication
- the MainApplication
box on the image corresponds to any of our sample application projects.
MockupServiceLayer
contains an implementation of IBookStoreDataServiceAccessor
(this interface is defined in BookStoreInfrastructure
project). TheBootstrapper
class of the main project is choosing the implementation of IBookStoreDataServiceAccessor
to be MockupDataAccessor
from the MockupServiceLayer
project.
MockupServiceLayer
contains functionality that returns a collection of items from BookStoreRecordVMs
project. This is why we have to split Record VM from Widget VM projects - the (mock or real) data service layer depends on Record VM project (it should know how to populate and return collections containing Record VM objects), while Widget VM project should depend on the data service layer (it should know how to call its API).
Patterns and Samples
Sample Code Location
All the samples are located under "WPFWidgetizer\Samples" folder.
Single Widget Sample
Important Note
We shall use this sample to give a detailed oveview of most of the WPFWidgetizer framework; so, you should read this sub-section carefully and perhaps even play with the corresponding code within the debugger.
Running the Sample
Project SingleWidgetSample
shows how to create a widget and load the data into it.
When you start the application, you see a blank window with the "Load Data from Server" button at its bottom:
After the button is pressed, for several seconds you'll see the following screen
Finally, after all the data is loaded, we see a data grid within the window:
Now, let us take a look at the code.
Main Project's Code
Here is the code from MainWindow.xaml file that defines the widget and the load button:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid>
<bookStoreWidgets:BookOrdersWidget x:Name="TheBookOrdersWidget" />
</Grid>
<Button x:Name="LoadDataButton"
Content="Load Data from Server"
Width="200"
Height="25"
Margin="0,10"
Grid.Row="1"/>
</Grid>
All the styles are connected via ResourceDictionary.MergeDictionary
statements at the top of the file:
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/WPFWidgetizer.GenericFramework.UIControls;Component/Themes/DataGridControlStyles.xaml" />
<ResourceDictionary Source="/WPFWidgetizer.BookStoreSpecificFramework.BookStoreWidgets;Component/Themes/BookWidgetStylesReferences.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
The code-behind file MainWindow.xaml.cs has the following handler for the Click
event of the LoadDataButton
:
async void LoadDataButton_Click(object sender, RoutedEventArgs e)
{
await TheBookOrdersWidget.LoadData();
}
View Model Class Hierarchy
Before venturing further, let us take a peek at the View Model class hierarchy:
Some of the classes in the hierarchy have generic type arguments which I skipped on the diagram in order to make it a little more readable.
The classes that we deal with in the samples are located at the bottom of the hierarchy, but we'll start describing the View Model classes from the top.
WidgetCommunicationsBase
is a class providing some wirings for inter-widget communications via Prism's EventAggregator
. We'll discuss it in detail later.
WidgetBaseVM
provides functionaltiy for defining various widget states (Loading
, Loaded
or Exception
). Its method LoadData()
controls is in control of the changing Widget state. It also calls method LoadDataImpl()
that is responsible for fetching data from the real or mock data service. LoadDataImpl()
is defined as an abstract method in WidgetBaseVM
and should be overridded in one of its subclasses.
WidgetWithItemsCollectionVM<RecordType>
is a View Model representing a Widget containing a collection of records. It contains ItemsSource
property to hold the collection. LoadDataImpl()
function defined in this class calls an abstract function GetData
to fetch the records from the service layer and populate ItemsSource
property.
WidgetWithHighlightableItemsCollectionVM<RecordType>
in addition to the functionality provided by WidgetWithItemsCollectionVM<RecordType>
class, also allows marking some rows as "highlighted", as will be detailed later.
BookStoreWidgetWithHighlightableItemsCollectionVM<RecordType>
provides an implementation of GetData()
method (declared abstract in the superclass). It also defines book store specific TheServiceRequestType
and TheInputParams
properties required for getting data from the server.
BookOrdersWidgetVM
and BookReviewsWidgetVM
are realizations of BookStoreWidgetWithHighlightableItemsCollectionVM<RecordType>
specifying the concrete RecordType
and setting some of the properties within their contstructor.
Overview or the View Model Functionality and Data Fetching Functionality
Now take a look at the widget's LoadData()
method's implementation. This should give you a pretty good idea of how the data is loaded into a single widget.
LoadData()
method is implemented by WidtgetWithContext<WidgetVMType>
class located within WPFWidgetizer.GenericFramework.WidgetsAndAssemblies
project and namespace. All this method does, is - it calls the corresponding LoadData()
method on the View Model of the widget:
public override async Task LoadData()
{
await TheVM.LoadData();
}
The View Model's LoadData()
method is implemented by WidgetVM
class of WPFWidgetizer.GenericFramework.WidgetVMs
project (and namespace):
public async Task LoadData()
{
this.TheLoadingState = WidgetLoadingState.Loading;
try
{
await LoadDataImpl();
this.TheLoadingState = WidgetLoadingState.Loaded;
}
catch(Exception e)
{
this.TheLoadingState = WidgetLoadingState.Exception;
this.LastDataLoadingException = e;
}
}
As explained in the comments, LoadData()
method controls the Widget's state while the data loading takes place. Widget State is defined by TheLoadingState
property of the Widget's View Model. It is an Enumeration of WidgetLoadingState
type defined under WPFWidgetizer.GenericFramework.WidgetVMs
project (namespace).
In order to actually get data (from a real or mock server/service), LoadData()
calls abstract method LoadDataImpl()
. This method is overridden in the one of the subclasses of WidgetBadgeVM
class.
For this sample, we are using BookOrdersWidget
. It is derived from WidgetWithHighlightableDataGrid<BookOrdersWidgetVM, BookOrderRecordVM>
class. The Generic type arguments of the latter class specify the View Model of the whole widget and the View Model of each row within the Widget's grid correspondingly:
public class BookOrdersWidget :
WidgetWithHighlightableDataGrid<BookOrdersWidgetVM, BookOrderRecordVM>
{
}
The Widget's View Model type is thus BookOrdersWidgetVM
, which is derived from BookStoreWidgetWithHighlightableItemsCollectionVM<BookOrderRecordVM>
class:
public class BookOrdersWidgetVM :
BookStoreWidgetWithHighlightableItemsCollectionVM<BookOrderRecordVM<
{
public BookOrdersWidgetVM()
{
this.TheServiceRequestType = BookStoreDataServiceRequestType.BookOrder;
PropertyNameToHightlightOn = "BookCode";
}
}
BookStoreWidgetWithHighlightableItemsCollectionVM<RecordType>
is in turn derived from WidgetWithHighlightableItemsCollectionVM<RecordType>
class.
WidgetWithHighlightableItemsCollectionVM<RecordType>
provides some plumbing for row highlighting which will be discussed later. At this point, however we are interested in it's base class (superclass) WidgetWithItemsCollection<RecordType>
which defines the ItemsSource
property for holding the collection of records for the Widget. It also provides an implementation for LoadDataImpl()
method that calls another abstract function GetItemsSource()
for retrieving the widget's records from real or mockup data service layer:
public abstract class WidgetWithItemsCollectionVM<RecordType> :
WidgetBaseVM
where RecordType : IRecordVM
{
#region ItemsSource Property
private IEnumerable<RecordType> _itemsSourcee;
public IEnumerable<RecordType> ItemsSource
{
get
{
return this._itemsSourcee;
}
set
{
if (this._itemsSourcee == value)
{
return;
}
this._itemsSourcee = value;
this.OnPropertyChanged("ItemsSource");
}
}
#endregion ItemsSource Property
protected abstract Task<IEnumerable<RecordType>> GetItemsSource();
public async override Task LoadDataImpl()
{
ItemsSource = await GetItemsSource();
}
}
The implementation of GetItemsSource()
method is located in a View Model class we've discussed before - BookStoreWidgetWithHighlightableItemsCollectionVM<RecordType>
. This is a base class for all the BookStore widgets that contain a data grid. Here is the code:
protected async override Task<IEnumerable<RecordType>> GetItemsSource()
{
IEnumerable<RecordType> result =
await BookStoreDataServiceAccessorSingleton.
TheDataServiceAccessor.
LoadServiceData<RecordType>
(
this.TheServiceRequestType,
this.TheInputParams
);
return result;
}
We contact the data accessor singleton calling its LoadServiceData(...)
method and passing the Widget's TheServiceRequestType
and TheInputParams
arguments.
TheServiceRequestType
usually corresponds to the name of the service we want to call (e.g. it can be a name of a DB stored procedure), while TheInputParams
property is just a container of the name-value pairs that specify what parameters we want to pass to that service.
Both these parameters are defined within the same class BookStoreWidgetWithHighlightableItemsCollectionVM<RecordType>
. Note, however, that we will probably need these parameter for any Book Store related request not only for those with "highlightable item collections". We can factor these two properties out in a separate super-class, but then because of the lack of multiple implementation inheritance in C#, we'll have trouble attaching them to our class, since it is already derived from WidgetWithHighlightableItemsCollectionVM<RecordType>
. Because of this problem, we are forced to re-implement these properties in all the Book Store View Models that do not derive from our class. We did, the next best thing, however, and factored these two properties out into IBookStoreWidget
interface so that we could use various Book Store View Model classes in a unified way. Our BookStoreWidgetWithHighlightableItemsCollectionVM<RecordType>
class implements this interface and provides an implementation for these two properties.
Let us go back to the implementation of our GetItemsSource()
function:
protected async override Task<IEnumerable<RecordType>> GetItemsSource()
{
IEnumerable<RecordType> result =
await BookStoreDataServiceAccessorSingleton.
TheDataServiceAccessor.
LoadServiceData<RecordType>
(
this.TheServiceRequestType,
this.TheInputParams
);
return result;
}
It uses LoadServiceData<RecordType>(...)
method declared within IGenericDataServiceAccessor<DataServiceRequestTypeEnum>
interface:
public interface IGenericDataServiceAccessor<DataServiceRequestTypeEnum>
where DataServiceRequestTypeEnum : struct, IConvertible
{
Task<IEnumerable<T>> LoadServiceData<T>
(
DataServiceRequestTypeEnum serviceRequestType,
ServiceRequestInputParams inputParams
);
}
Book Store specific data service accessor IBookStoreDataServiceAccessor
inherits from IGenericDataServiceAccessor<DataServiceRequestTypeEnum>
interface and is MEFable:
[InheritedExport(typeof(IBookStoreDataServiceAccessor))]
public interface IBookStoreDataServiceAccessor : IGenericDataServiceAccessor<BookStoreDataServiceRequestType>
{
}
GenericDataServiceAccessorSingleton<DataServiceAccessor, DataServiceRequestTypeEnum>
class pulles and implementation of DataServiceAccessor
type out of the MEF container:
public static DataServiceAccessor TheDataServiceAccessor
{
get
{
if (TheAppIoCContainer.TheCompositionContainer == null)
return null;
DataServiceAccessor result =
TheAppIoCContainer.
TheCompositionContainer.
GetExportedValue<DataServiceAccessor>();
return result;
}
}
BookStoreDataServiceAccessorSingleton
class calls the functionality of GenericDataServiceAccessorSingleton<DataServiceAccessor, DataServiceRequestTypeEnum>
to return the current implementation of IBookStoreDataServiceAccessor
interface.
TheBootstrapper
class of the main project takes care of bootstrapping the correct implementation of IBookStoreDataServiceAccessor
interface (in our case it is MockupDataAccessor
class of WPFWidgetizer.TestAndMockupUtils.MockupServiceLayer
project (namespace):
public class TheBootstrapper : AppBootstrapper<mainwindow>
{
protected override void ConfigureAggregateCatalog()
{
base.ConfigureAggregateCatalog();
AggregateCatalog.Catalogs.Add(new AssemblyCatalog(typeof(MockupDataAccessor).Assembly));
}
}
</mainwindow>
Here is the implementation of MockupDataAccessor
class that's actually being called:
public class MockupDataAccessor : IBookStoreDataServiceAccessor
{
public async Task<IEnumerable<T>> LoadServiceData<T>
(
BookStoreDataServiceRequestType serviceRequestType,
ServiceRequestInputParams inputParams
)
{
switch(serviceRequestType)
{
case BookStoreDataServiceRequestType.BookOrder:
await Task.Delay(2000);
return new MockBookOrderRecords() as IEnumerable<T>;
case BookStoreDataServiceRequestType.BookReview:
await Task.Delay(3000);
return new MockBookReviewRecords() as IEnumerable<T>;
case BookStoreDataServiceRequestType.Book:
return new MockBooks() as IEnumerable<T>;
default:
return null;
}
}
}
When we call it from BookOrdersWidgetVM
we are passing ServiceRequestType
argument as BookStoreDataServiceRequestType.BookOrder
; correspondingly, it returns MockBookOrderRecords
collection.
UI Widget Class Hierarchy
We've given a good review of the View Models, now let us look at the UI classes and the XAML code.
We'll start by showing the class hierarchy diagram for Widget UI classes and by giving brief overview of every class within it.
Here a brief description of the UI classes within this hierarchy (more detailed explanations will follow below).
WidgetBase
class provides plumbing for communications with the View Model. It has several properties for defining a Widget's header. It's XAML Style/Template takes care of displaying different screens for different Widget's state ('Loading', 'Loaded', 'Exception) and for displaying the Widget's header.
WidgetWithContext<WidgetVMType>
creates a View Model object of type WidgetVMType
and sets the Widget's Content
property to that object within the Widget's constructor. Also provides the LoadData()
method that calls method of the same name on the View Model.
WidgetWithHighlightableDataGrid<WidgetVMType, RecordType>
provides an extra property allowing to specify a collection of data grid columns.
BookOrdersWidget
and BookReviewsWidget
are simply concrete realization of WidgetWithHighlightableDataGrid<WidgetVMType, RecordType>
with WidgetVMType
and RecordType
generic arguments set to concrete types.
UI Widget Code and Styles
Now, we'll continue describing how the WPFWidgetizer's functionality is being used by our sample.
As was mentioned above, we use BookOrdersWidget
to display the returned entries. This class inherits from WidgetWithHighlightableDataGrid<BookOrdersWidgetVM, BookOrderRecordVM>
class, which in turn inherits from WidgetWithContext<WidgetVMType>
. The latter class provides a way to initialize the View Model for the Widget. It extends WidgetBase
class which is derived from WPF's ContentControl
class.
As was shown above, WidgetBase
is the topmost class of the hierarchy. It derives from WPF's ContentControl
. It's Content
property is set to the Widget's View Model, while its ContentTemplate
(in XAML) to the Widget's DataTemplate
. It provides some UI action plumbing (which will be explained later). It also has dependency properties WidgetCaption
and ShowWidgetHeader
which define what should be shown within the Widget's header and whether the Widget's header should be shown at all.
The most important dependency property of the class is WidgetDataContentTemplate
. The value for this dependency property is usually provided within the Styles of the derived classes. WidgetDataContentTemplate
defines the actual representation of the data successfully loaded into the Widget.
WidgetBase
style is defined within Themes/WidgetStyles.xaml file of the WPFWidgetizer.GenericFramework.WidgetsAndAssemblies
project. It sets the ContentTemplate
property of the widget to the following:
<DataTemplate>
<Grid x:Name="TopLevelWidgetContentContainerPanel">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
-->
<Border x:Name="HeaderBorder"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
BorderBrush="Black"
BorderThickness="1"
Height="30"
Visibility="{Binding Path=ShowWidgetHeader,
Converter={StaticResource TheBooleanToVisibilityConverter},
RelativeSource={RelativeSource AncestorType=widgets:WidgetBase}}">
<TextBlock x:Name="WidgetCaption"
Text="{Binding Path=WidgetCaption,
RelativeSource={RelativeSource AncestorType=widgets:WidgetBase}}"
FontSize="15"
HorizontalAlignment="Left"
VerticalAlignment="Bottom"
Margin="10,0,0,3"/>
</Border>
-->
<Grid x:Name="WidgetContentPanel"
Grid.Row="1">
-->
<ContentControl x:Name="TheWidgetContent"
Content="{Binding}"
ContentTemplate="{Binding Path=WidgetDataContentTemplate,
RelativeSource={RelativeSource AncestorType=widgets:WidgetBase}}"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
Visibility="Collapsed"/>
-->
<Grid x:Name="ErrorDisplayPanel"
Visibility="Collapsed">
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="Red"
FontSize="30"
TextWrapping="WrapWithOverflow"
Text="{Binding Path=LastDataLoadingException.Message}"
Margin="20"/>
</Grid>
-->
<Grid x:Name="LoadingPanel"
Visibility="Collapsed">
<TextBlock Text="Please wait while the data is loading"
Foreground="Black"
FontSize="30"
Margin="20"
TextWrapping="WrapWithOverflow"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Grid>
</Grid>
</Grid>
<DataTemplate.Triggers>
-->
<DataTrigger Binding="{Binding Path=TheLoadingState}"
Value="Loaded">
<Setter TargetName="TheWidgetContent"
Property="Visibility"
Value="Visible" />
</DataTrigger>
-->
<DataTrigger Binding="{Binding Path=TheLoadingState}"
Value="Loading">
<Setter TargetName="LoadingPanel"
Property="Visibility"
Value="Visible" />
</DataTrigger>
-->
<DataTrigger Binding="{Binding Path=TheLoadingState}"
Value="Exception">
<Setter TargetName="ErrorDisplayPanel"
Property="Visibility"
Value="Visible" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
HeaderBorder
elements defines the Header of the Widget. Its visibility and text are controlled by the Widget's ShowWidgetHeader
and WidgetCaption
dependency properties correspondingly.
Within the widget's body (defined by WidgetContentPanel
Grid
) there are three different parts TheWidgetContent
ContentControl
, ErrorDisplayPanel
Grid
and LoadingPanel
Grid
. They are never shown together, but each one corresponds to a Widget's state (defined by the Widget's TheLoadingState
property).
TheWidgetContent
is visible when the Widget's state is Loaded
- meaning the call to the server/service successfully returned data. TheWidgetContent
's Content
property is bound to the Widget's View Model, while its ContentTemplate
is bound to WidgetDataContentTemplate
property provided at the Widget's level. Using this property, we can choose the DataTemplate
for the Widget's data.
ErrorDisplayPanel
is shown when data loading resulted in an exception (the Widget's state is Exception
).
LoadingPanel
is shown while the data is being loaded - the Widget's state is Loading
. This panel displays the message "Please wait while the data is loading" that we observed above.
While running the sample above, we already showed the Widget's Loading
and Loaded
states. It is easy to test the Exception
state also. We have to go to the MockupDataAccessor
class and uncomment the line above the switch
statement throwing an exception:
After re-running the application and clicking "Load Data" button, we get the following screen:
Now, that we understand the mechanics of the Widget's states (provided by WidgetBase
class and the corresponding Styles/Templates), let us talk about how the Widget's data content is displayed in case of successful data loading.
Here is the code for BookOrdersWidget
class:
public class BookOrdersWidget :
WidgetWithHighlightableDataGrid<BookOrdersWidgetVM, BookOrderRecordVM>
{
}
As you see - it is simply a realization of WidgetWithHighlightableDataGrid
class with the Widget's View Model being BookOrdersWidgetVM
and the row (record) View Model being BookOrderRecordVM
.
WidgetWithHighlightableDataGrid<WidgetVMType, RecordType>
has only one interesting dependency property: GridColumns
:
public IEnumerable<datagridcolumn> GridColumns {...}
</datagridcolumn>
This property allows to specify the GridColumn
collection at the Widget's level.
Here is the XAML code for WidgetWithHighlightableDataGrid
Style located within "Themes/WidgetWithHighlightableDataGridStyles.xaml" file of the WPFWidgetizer.GenericFramework.WidgetsAndAssemblies
project:
<Style TargetType="widgets:WidgetBase"
BasedOn="{StaticResource TheBaseWidgetStyle}"
x:Key="WidgetWithHighlightableGridStyle">
<Setter Property="WidgetDataContentTemplate">
<Setter.Value>
<DataTemplate>
<uiControls:DataGridControl x:Name="PART_TheDataGrid"
IsReadOnly="True"
ItemsSource="{Binding Path=ItemsSource}"
CanHighlight="{Binding Path=CanHighlight}"
GridColumns="{Binding Path=GridColumns, RelativeSource={RelativeSource AncestorType=widgets:WidgetBase}}">
<uiControls:DataGridControl.RowStyle>
<Style TargetType="DataGridRow"
BasedOn="{StaticResource BaseDataGridRowStyle}">
<Setter Property="uiControls:AttachedProperties.IsRowHighlighted"
Value="{Binding Path=IsHighlighted}" />
</Style>
</uiControls:DataGridControl.RowStyle>
</uiControls:DataGridControl>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
Note, that the TargetType
of the Style
is WidgetBase
, not WidgetWithHighlightableDataGrid<WidgetVMType, RecordType>
as one would expect. This is because compiled XAML still cannot handle the generics, so we are forced to specify a non-generic superclass as the TargetType
.
We use the Widget's dependency property WidgetDataContentTemplate
explained above, to specify the data template for the Widget's data.
Inside the DataTemplate
you can see DataGridControl
object that displays the grid. GridColumns
property of the DataGridControl
object is bound to the GridColumns
property of the Widget, so that we can specify the collection of GridColumns
at the Widget's level.
DataGridControl
class is defined under WPFWidgetizer.GenericFramework.UIControls
project. It extends WPF's DataGrid
class and it defines two dependency properties: CanHighlight
(that controls whether the data grid's rows can be highlighted) and GridColumns
property.
We need GridColumns
property since the DataGrid
's built-in Columns
property is read-only and cannot become a WPF binding's target. We use GridColumn
property of the DataGridControl
to modify the Columns
collection. GridColumn
as readable/writable dependency property can be made a WPF binding's target.
The Style for BookOrdersWidget
is defined within "BookOrderWidgetStyles.xaml" file under WPFWidgetizer.BookStoreSpecificFramework.BookStoreWidgets
project. It is based on WidgetWithHighlightableGridStyle
and defines the GridColumns
we want to show within the widget:
<Style TargetType="bookStoreWidgets:BookOrdersWidget"
BasedOn="{StaticResource WidgetWithHighlightableGridStyle}">
<Setter Property="GridColumns">
<Setter.Value>
<x:Array Type="DataGridColumn">
<DataGridTextColumn Header="Code"
Binding="{Binding Path=BookCode}" />
<DataGridTextColumn Header="Title"
Binding="{Binding Path=BookTitle}" />
<DataGridTextColumn Header="Number Copies Ordered"
Binding="{Binding Path=NumberCopies}" />
<DataGridTextColumn Header="Order Price"
Binding="{Binding Path=TotalOrderPrice, StringFormat=F2}" />
</x:Array>
</Setter.Value>
</Setter>
</Style>
With this we complete our review of the first sample (as well as of most of the WPFWidgetizer code).
Simple Widget Assembly Consisting of Two Widgets with Inter-Widget Communications Sample
Sample Introduction
This sample is located within SimpleWidgetAssemblySample.sln
solution under "WPFWidgetizer/Samples/SimpleWidgetAssemblySample" folder. The main project of the sample is WPFWidgetizer.Samples.SimpleWidgetAssemblySample
.
While the purpose of the previous sample was to show the generic code structure of the package and demonstrate how the data is fetched from a service into the widget, this sample is primarily concentrated on describing the mechanism for sending action requests from within the widget and communicating between the widgets.
Another purpose of this sample is to show how one can use WPFWidgetizer code to arrange several widgets to work together within the same Widget Assembly. Note, that only arranging of the Widgets will require a custom XAML file - the communications between the Widgets are generic and do not require any extra code specific to this Widget Assembly.
Running the Sample
After starting the sample, you'll see the following screen:
Now, press "Load Data from Server" button. For several seconds you'll be seeing the following:
Finally, after the data loads, here what you'll see:
We have a table of book reviews and a table of book orders displayed next to one another. If you click on a row in one of these tables, the rows corresponding to the same book in the other table will be highlighted in pink:
Clicking a different row in the reviews grid would result in a change the set of highlighted rows in the orders grid. Similarly, clicking on one of the rows within orders grid, will result in rows corresponding to the same book being highlighted in the reviews grid.
Some Architectural Challenges
Achieving the behaviors demonstrated above presented several architectural challenges:
- The message is being sent from one widget to another when a row within
DataGridControl
is clicked. DataGridControl
is a basic control from a basic project UIControls
that should not know anything about Widgets or inter-Widget messaging. We need some kind of a plumbing to bring the action that occurred deep within the widget onto the Widget level before sending the corresponding message.
- What if there are more than two widgets within the Application. How do they know which widget should communicate to which? We need some kind of selective messaging between the widgets.
Bringing a Control's UI Action to the Widget's Level
In order to resolve the first architectural challenge listed above we defined the UIActionEvent
bubbling routed event within UIRoutedEvents
static class defined under UIControls
project:
public delegate void UIActionEventHandler(object sender, UIActionEventArgs e);
public static class UIRoutedEvents
{
public static readonly RoutedEvent UIActionEvent =
EventManager.RegisterRoutedEvent
(
"UIAction",
RoutingStrategy.Bubble,
typeof(UIActionEventHandler),
typeof(UIRoutedEvents)
);
}
This event will fire when the corresponding user action occurs on a control and will bubble to the Widget's level to be handled by the Widget.
C# code for handling UIActionEvent
is part of WidgetBase
implementation:
public WidgetBase()
{
this.AddHandler(UIRoutedEvents.UIActionEvent, (UIActionEventHandler) OnUIActionHappened);
}
WidgetCommunicationsBase TheWidgetCommunicationsVM
{
get
{
return this.Content as WidgetCommunicationsBase;
}
}
void OnUIActionHappened(object sender, UIActionEventArgs e)
{
TheWidgetCommunicationsVM.OnUIAction(e.TheUIActionType, e.Payload);
}
We attach the OnUIActionHappened
handler to the routed event within the WidgetBase()
constructor. Within the handler's code, we simply pass the TheUIActionType
and Payload
properties of the event arguments to the Widget's View Model.
Let us take a look at the UIActionEvent
's arguments. They are represented by UIActionEventArgs
class defined within UIControls
project:
public class UIActionEventArgs : RoutedEventArgs
{
public object Payload { get; set; }
public UIActionType TheUIActionType { get; set; }
}
Payload
can be any object (in our case it is set to the View Model of the clicked row). UIActionType
is an enumeration defined under GenericInfrastructure
project, so that it can be available to both, the UI controls and View Models. At this point this enumeration has only two entries:
public enum UIActionType
{
Unknown,
RowClick
}
Now, take a look at OnUIAction<T>
method within WidgetCommunicationsBase
class. This class, BTW, is the topmost class within Widget's View Model hierarchy and, as was mentioned above, it handles communications.
Here is the code for OnUIAction<T>
:
public void OnUIAction<T>(UIActionType uiActionType, T payload)
{
if (!_uiActionHandlers.ContainsKey(uiActionType))
return;
Action<object> handler = _uiActionHandlers[uiActionType];
handler(payload);
}
Dictionary _uiActionHandlers
maps the UIActionType
s into handler delegates of type Action<object>
:
Dictionary<uiacti>> _uiActionHandlers =
new Dictionary<uiacti>>();
</uiacti></uiacti>
If the UIActionType
passed from the widget is contained in this _uiActionHandlers
dictionary as a key, we pull out the handler value and call it, passing Payload
to it as a parameter.
In order to set the mapping between a UIActionType
and a handler, we use RegisterUIActionHandler<T>
function:
public void RegisterUIActionHandler<T>(UIActionType uiActionType, Action<T> handler)
{
_uiActionHandlers[uiActionType] = (obj) => handler((T) obj);
}
In our case, the handler for RowClick
UIActionType
is being set within the constructor of the WidgetWithHighlightableItemsCollectionVM
class:
public WidgetWithHighlightableItemsCollectionVM()
{
RegisterUIActionHandler<RecordType>(UIActionType.RowClick, OnItemClicked);
...
}
private void OnItemClicked(RecordType item)
{
this.CanHighlight = false;
this.Publish<object>(item, WidgetMessageType.Highlight);
}
As you can see, a row click results in two things happening:
CanHighlight
property on the View Model is set to false. This is done in order to remove highlighting from the data grid whose row was clicked.
- We publish an inter-widget message of
WidgetMessageType.Highlight
message type passing the data received from the UIActionEvent
as a payload.
To complete this sub-section, let us show the mechanism by which we fire the UIActionEvent
from the control. It is being attached to the grid rows via ClickRowBehavior
within "Themes/DataGridControlStyles.xaml" resource file of WPFWidgetizer.GenericFramework.UIControls
project:
<uiControls:ClickRowBehavior x:Key="TheClickRowBehavior" />
<Style TargetType="DataGridRow"
x:Key="BaseDataGridRowStyle">
<!-- Set the ClickRowBehavior to fire UIActionEvent routed event
when a row is clicked on the grid -->
<Setter Property="uiControls:AttachedProperties.TheBehavior"
Value="{StaticResource TheClickRowBehavior}" />
<Style.Triggers>
<!-- set the widget's background to pink
if IsRowHighlighted attached property on is true on the row
and CanHighlight property is true on the Widget -->
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Path=(uiControls:AttachedProperties.IsRowHighlighted),
RelativeSource={RelativeSource Self}}"
Value="True" />
<Condition Binding="{Binding Path=CanHighlight,
RelativeSource={RelativeSource AncestorType=uiControls:DataGridControl}}"
Value="True" />
</MultiDataTrigger.Conditions>
<Setter Property="Background"
Value="Pink" />
</MultiDataTrigger>
</Style.Triggers>
</Style>
The behaviors have been described in WPF Control Patterns. (WPF and XAML Patterns of Code Reuse in Easy Samples. Part 1). We attach our ClickRowBehavior
via AttachedProperties.TheBehavior
attached property. When attached to an object, the behavior's Attach(...)
method is called assigning event handlers to the object's events. Here is the code for ClickRowBehavior
class:
public class ClickRowBehavior : IBehavior
{
public void Attach(FrameworkElement element)
{
element.MouseLeftButtonUp += element_MouseLeftButtonUp;
}
void element_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
FrameworkElement element = (FrameworkElement) sender;
UIActionEventArgs uiActionEventArgs = new UIActionEventArgs
{
TheUIActionType = UIActionType.RowClick,
RoutedEvent = UIRoutedEvents.UIActionEvent
};
uiActionEventArgs.Payload = element.DataContext;
element.RaiseEvent(uiActionEventArgs);
}
public void Detach(FrameworkElement element)
{
element.MouseLeftButtonUp -= element_MouseLeftButtonUp;
}
}
Inter-Widget Communications
Now we are going to shed light on the WPFWidgetizer's mechanism for communications between various widgets.
As you rembember, we explained in the previous sub-section, how our row click event would result in Publish(...)
method being called on the Widget's View Model (see the code for OnItemClick(...)
function within WidgetWithHighlightableItemsCollectionVM
class:
private void OnItemClicked(RecordType item)
{
this.CanHighlight = false;
this.Publish<object>(item, WidgetMessageType.Highlight);
}
Let us take a look at the implementation of Publish(...)
function, located within WidgetCommunicationsBase
class:
public void Publish<T>
(
T messagePayload,
WidgetMessageType messageType,
CommunicationsScopeType messageScopeType = CommunicationsScopeType.None,
long targetWidgetID = -1
)
{
long scopeItemID = -1;
if ( (_widgetScopeResolver != null) &&
(messageScopeType != CommunicationsScopeType.None))
{
scopeItemID = _widgetScopeResolver.GetIDByScopeType(messageScopeType);
}
WidgetMessage<T> widgetMessage = new WidgetMessage<T>
{
SenderID = this.UniqueID,
ScopeItemID = scopeItemID,
TheMessageScopeType = messageScopeType,
TargetWidgetID = targetWidgetID,
MessagePayload = messagePayload,
TheWidgetMessageType = messageType
};
EventAggregatorSingleton.Publish<WidgetMessage<T>>(widgetMessage);
}
This function shows how the inter-Widget message is being formed.
Each Widget has its own unique id contained within its UniqueID
property (coming, BTW, from IUniqueIDContainer
interface that all the widgets implement). The Publish(...)
function sets SenderID
of the WidgetMessage
object to be the UniqueID
of the current Widget.
MessagePayload
property of the WidgetMessage
carries the data for the message.
TheWidgetMessageType
property of the WidgetMessage
can be used to narrow down the application of the current message - each widget can register to receive only messages of a certain WidgetMessageType
. WidgetMessageType
is an enumeration defined within WPFWidgetizer.GenericFramework.WidgetVMs
project:
public enum WidgetMessageType
{
Unknown,
Highlight,
Navigate
}
There are two modes of WidgetMessage
propagation - Broadcast mode and Link mode. Both these modes will be discussed in more detail with more relevant samples in the follow-up article. Here, we are only going to give their brief overview.
Broadcast mode, will result in the WidgetMessage
propagation to all the widgets within the Widget hierarchy (remember there can be Widgets containing Widget Assemblies and sub-Widgets and we are going to talk about them in the follow-up article). For broadcast mode, ScopeItemID
property set on the WidgetMessage
determines the topmost object under which all the widgets that share this object with the sender will receive the message from the sender Widget. If the ScopeItemID
is -1
, the message will be propagated to all the widgets within the application.
In Link mode, two widgets are linked and the sender Widget knows the UniqueID
of the target Widget. Within WidgetMessage
we set use TargetWidgetID
property to specify it.
Of course, there can be combinations of the two approaches with a Widget sending messages to a target Widget within the same scope, assuming that other scopes within the Widget hierarchy might have the same UniqueID
. This, BTW, can happen, if we have our link information serialized and restored. This will also be discussed in the follow-up article.
For our simple Widget Assembly example it is quite sufficient to keep TargetWidgetID
set to -1
(meaning there is no linking and all the widgets can get the message).
Now, let us look at what happens on the receiving side of the inter-Widget messages.
We register our inter-Widget message handler within the constructor of the WidgetWithHighlightableItemsCollectionVM
class:
public WidgetWithHighlightableItemsCollectionVM()
{
...
RegisterHandlerForMessageType<object>
(
WidgetMessageType.Highlight,
OnHighlightMessageArrived
);
}
OnHighlightMessageArrived(...)
method is called upon arrival of WidgetMessage
of WidgetMessageType.Highlight
message type:
private void OnHighlightMessageArrived(object item)
{
if (ItemsSource == null)
return;
object highlightPropValue = item.GetProp(PropertyNameToHightlightOn);
if (highlightPropValue == null)
return;
this.CanHighlight = true;
foreach (HighlightableBaseRecord record in this.ItemsSource)
{
object recordHighlightPropValue = record.GetProp(PropertyNameToHightlightOn);
record.IsHighlighted = (recordHighlightPropValue == highlightPropValue);
}
}
PropertyNameToHightlightOn
contains a name of the property whose value we compare in each record within the grid and from the incoming item object. If those values match, we highlight the row, otherwise we unhighlight it.
Function RegisterHandlerForMessageType
that we used to associate OnHighlightMessageArrived(...)
method with WidgetMessageType.Highlight
is defined within WidgetCommunicationBase
class:
public void RegisterHandlerForMessageType<T>
(
WidgetMessageType messageType,
Action<T> messageHandler
)
{
PrismUnsubscriber unsubscriber =
EventAggregatorSingleton.Subscribe<WidgetMessage<T>>
(
(msg) => messageHandler(msg.MessagePayload),
(msg) => FilterMsg<T>(msg, messageType)
);
List<PrismUnsubscriber> subscriptionsTokensForMessageType;
if (!_messageSubscriptions.TryGetValue(messageType, out subscriptionsTokensForMessageType))
{
subscriptionsTokensForMessageType = new List<PrismUnsubscriber>();
_messageSubscriptions[messageType] = subscriptionsTokensForMessageType;
}
subscriptionsTokensForMessageType.Add(unsubscriber);
}
The subscription itself happens at the top of the function in the call to EventAggregatorSingleton.Subscribe(...)
method. The rest of the function's body is related to storing the subscription tokens in order to be able to unsubscribe in the future.
There are two arguments in the call to EventAggregatorSingleton.Subscribe(...)
. The first argument is simply the message handler to be called when the message satisfying all the requirements arrives. The second argument is a predicate that we use to filter in only messages that satisfy all the requirements. As you can see, we pass method FilterMsg(...)
as the second argument to this Subscribe
call.
Here is the code for FilterMsg(...)
function:
bool FilterMsg<T>(WidgetMessage<T> msg, WidgetMessageType widgetMessageType)
{
if (!IsActive)
return false;
if (msg.TheWidgetMessageType != widgetMessageType)
return false;
if (msg.TargetWidgetID >= 0)
{
if (msg.TargetWidgetID != this.UniqueID)
return false;
}
if (msg.SenderID == this.UniqueID)
return false;
if ((msg.ScopeItemID != -1) && (msg.TheMessageScopeType != CommunicationsScopeType.None))
{
if (_widgetScopeResolver == null)
return false;
long scopeID = _widgetScopeResolver.GetIDByScopeType(msg.TheMessageScopeType);
if (scopeID != msg.ScopeItemID)
return false;
}
return true;
}
What the method does is highlighted in its comments.
- First we check if the Widget is active, if not we return false and the handler is not called.
- Then we check if the
WidgetMessageType
of the message and the current handler match. If not, we do not call the handler.
TargetWidgetID
of the message being non-negative means that we are using the Link mode for the messaging and only the Widgets whose UniqueID
coincides with the one of the message should fire their handlers.
- If the
ScopeItemID
of the message is non-negative and TheMessageScopeType
is not 'None' we make sure that the current widget belongs to the same scope (as will be explained in detail in the follow-up article).
Widget Assembly for BookOrdersWidget and BookReviewsWidget
Widget Assembly for our sample is being defined by a Style/Template within "Themes/BookReviewsAndOrdersAssemblyStyles.xaml" file under WPFWidgetizer.BookStoreSpecificFramework.BookStoreWidgets
project:
<Style x:Key="TheBookReviewsAndOrdersAssemblyStyle"
TargetType="widgets:WidgetAssembly">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="widgets:WidgetAssembly">
<Grid x:Name="PART_WidgetAssemblyPanel">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<bookStoreWidgets:BookReviewsWidget x:Name="TheBookReviewsWidget"
WidgetCaption="Book Reviews"
ShowWidgetHeader="True"/>
<bookStoreWidgets:BookOrdersWidget x:Name="TheBookOrdersWidget"
Grid.Column="1"
WidgetCaption="Book Orders"
ShowWidgetHeader="True"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Note that its only purpose it to arrange the two widgets. It has no code specific to inter-Widget communications or interaction.
WidgetAssembly
class is defined under WPFWidgetizer.GenericFramework.WidgetsAndAssemblies
project:
public class WidgetAssembly : Control
{
Panel _widgetAssemblyPanel = null;
public string WidgetAssemblyName
{
get;
set;
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_widgetAssemblyPanel = this.Template.FindName("PART_WidgetAssemblyPanel", this) as Panel;
}
public IEnumerable<WidgetBase> TheAssemblyWidgets
{
get
{
List<WidgetBase> result = new List<WidgetBase>();
foreach (var child in _widgetAssemblyPanel.Children)
{
WidgetBase assemblyWidget = child as WidgetBase;
if (assemblyWidget != null)
result.Add(assemblyWidget);
}
return result;
}
}
public async Task LoadData()
{
Task[] loadDataTasks = this.TheAssemblyWidgets.Select((widget) => widget.LoadData()).ToArray();
await Task.WhenAll(loadDataTasks);
}
}
TheAssemblyWidgets
property enumerates the child Widgets of the Widget Assembly, while LoadData()
method of the WidgetAssembly
calls the LoadData()
methods for all its child Widgets.
In the main project WPFWidgetizer.Samples.SimpleWidgetAssemblySample
we simply display the Widget Assembly within a grid panel.
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid>
<widgets:WidgetAssembly Style="{StaticResource TheBookReviewsAndOrdersAssemblyStyle}"
x:Name="TheWidgetAssembly"/>
</Grid>
<Button x:Name="LoadDataButton"
Grid.Row="1"
Content="Load Data from Server"
Width="200"
Height="25"
Margin="0,10"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Grid>
LoadDataButton
has its handler connected in MainWindow.xaml.cs
file to call LoadData()
method on the Widget Assembly:
[Export]
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.LoadDataButton.Click += LoadDataButton_Click;
}
async void LoadDataButton_Click(object sender, RoutedEventArgs e)
{
await TheWidgetAssembly.LoadData();
}
}
Conclusion
In this installment of the article we introduced WPFWidgetizer framework based architecture and also the Widget Assembly pattern. We explained how they can be used for building flexible and extensible WPF applications fast.
In the follow-up article, we plan to cover
- Widgets that contain Widget Assembly with sub-widgets within them.
- Scope based hierarchical inter-Widget communications.
- Navigation between different Widget Assemblies.
- Element Factory Pattern.
- View Model Hierarchy Pattern.