Composite UI Pattern and RAD Development for Data Entry Applications, Part 1

In this paper, a set of base classes will be introduced that facilitate the RAD development on the Composite UI pattern.

Introduction

Composite UI is one of the great patterns in the .NET framework that targets the large enterprise UI centric applications. The pattern makes it possible to break a very complex and heavy user interfaces into separate loosely coupled modules. The modules can be developed by different teams and the pattern insures the loosely coupling among the different parts such as modules, services and the main shell. From an enterprise point of view, the pattern is very useful, but to be honest with you, if you want to create an application in a RAD manner, you will encounter some difficulties in the composite UI pattern. There are lots of issues that slow down your hand in the pattern. For example, consider a simple application that shows a list of data and let users to do the CRUD (Create, Read, Update and Delete) operations. If you develop such application following the Composite UI standards, then you should create two presenters, two views, one CompositeEvent and one module at least. Using the pattern has many benefits, but it involves sacrificing the rapid application development. In this paper, I have tried to target the gap between Composite UI pattern and Rapid Application Development for common data entry scenarios. In other words, I will provide a library of classes that makes the process of creating data entry forms in the Composite UI easier.

Any application that deals with data has some kind of data entry forms. In most cases, the application shows a list of records for each type of data and the users can do the CRUD operations such as editing, deleting, adding, searching and etc on the data. For most of the applications, the data entry process contains the following UI components.

  • The simple list components that show a list of entities to users. Such components allow the user to see, traverse and search the items. Generally, users can do the "Add", "Edit", and "Delete" operations on the list too.
  • The Master-Detail components that show two list of items. The main list or master list contain the parent entities and the second list or detail list shows the childes of the selected item of the master list. Generally, users can do the "Add", "Edit" and "Delete" operations on both lists.
  • The Add-Edit components that show the input controls which allow the user to add the new entities or edit the current entities.
  • The Detail components that show the detail of an item.
  • The Main command bar that enable users to select an entity type to see its items.

Although it is not possible to formulate all windows and user controls that exist in an application, but the above components exist in many applications. Although such scenarios are very common, however there is not some kind of pattern in the Composite UI to cover the issue.

In this paper, I create a set of base classes that targets items 1, 3, and 4 in the above list. In the next paper, I will cover all of the above items and although a code generator that make it possible to create the presenters easier.

Developers can use the provided library to create the data entry forms in a few seconds. Composite UI uses the Model-View-Presenter patterns to create different parts of the user interface. In fact, an application that is based on Composite UI has some modules, those modules have some Presenters and those Presenters have one or more views. The modules stick the Views of Presenters to the regions of the main Shell. Generally, the code of Views is very thin and the communication among the Presenters will be done by Composite Commands and Composite Events. In this paper, I don't touch the Views, since the code of them is thin, but the focus is on base classes for Presenters.

A Summary of the Library

The following class diagram illustrates the whole classes in the library.

PresenterBase

It is the base class that both ListBasePresenters and ItemBasePresenters inherit from. The PresenterBase implements the INotifyPropertyChanged and IDataErrorInfo interfaces. Putting the implementation of those interfaces in the base class means there would be less code in concrete classes. The INotifyPropertyChanged has been used to notify the Views about the changes that have been done on the Presenter. Implementing the Interface is essential for most of the Presenters in the WPF platform. The IDataErrorInfo has been used to announce the validation errors to Views. The interface had existed since .NET 1.1. It was firstly used in DataSets to propagate the validation errors. Here is the code of the interface.

// Summary:

// Provides the functionality to offer custom error information that a user

// interface can bind to.

public interface IDataErrorInfo

{

// Summary:

// Gets an error message indicating what is wrong with this object.

//

// Returns:

// An error message indicating what is wrong with this object. The default is

// an empty string ("").

string Error { get; }

// Summary:

// Gets the error message for the property with the given name.

//

// Parameters:

// columnName:

// The name of the property whose error message to get.

//

// Returns:

// The error message for the property. The default is an empty string ("").

string this[string columnName] { get; }

}

IDataErrorInfo is my favorite way to propagate the validation errors from Presenters to Views. It was one the great missing features in the .NET framework 3.0, but the WPF team in Microsoft realized the issue and supported the interface in the .NET framework 3.5. Interested reader can find more about the interface and how to use it here. Here is the code of PresenterBase.

public class PresenterBase : IDataErrorInfo, INotifyPropertyChanged

{

#region IDataErrorInfo

public string Error

{

get

{

return null;

}

}

public string this[string name]

{

get

{

string result = "";

if (errors.ContainsKey(name))

{

result = errors[name];

}

return result;

}

}

private Dictionary<string, string> errors = new Dictionary<string, string>();

protected bool HasError(string key)

{

return errors.ContainsKey(key);

}

protected void AddError(string key, string value)

{

if (errors.ContainsKey(key))

{

errors[key] = value;

}

else

{

errors.Add(key, value);

}

PropertyChanged(this, new PropertyChangedEventArgs(key));

}

protected void ClearError(string key)

{

errors.Remove(key);

PropertyChanged(this, new PropertyChangedEventArgs(key));

}

protected void ClearErrors()

{

for (int i = errors.Count - 1; i > -1; i--)

{

string key = errors.Last().Key;

errors.Remove(key);

PropertyChanged(this, new PropertyChangedEventArgs(key));

}

}

#endregion

#region INotifyPropertyChanged

public event PropertyChangedEventHandler PropertyChanged = delegate { };

protected void notifyPropertyChanged(string propertyName)

{

if (PropertyChanged != null)

PropertyChanged(this, new PropertyChangedEventArgs(propertyName));

}

#endregion

}

ListBasePresenter<T, VIEW>

Any presenter that displays a list of items should inherit from this class. This class provides the basic functionalities that a list-based presenter needs to have. The first thing about the class is the fact that it is general. In fact, it should be a generic class so that the concrete classes that inherit from it could use its functionalities easily. There are two types that should be defined as generics in this class: 1) the type of the entity that the class shows its instances 2) the type of the View.

The class has the following properties.

  • View: Each Presenter should have at least one View. Putting the code of initializing the View in the base class would be a good idea, since its code has been repeated in Presenters
  • Items: ListBasePresenter is responsible for showing the items of type T. So, it should publish a list of type T.
  • Title: Hard coding the Title of the Presenter in its View may not be a good idea. It is better to have a Title property in the Presenter. The View can bind its title to the Title property of the Presenter.
  • IDataService<T>. The Presenter uses an instance of the IDataService<T> to load/save data. In the next section, I will describe the detail of the IDataService.
  • SelectedItem. In order to do the Edit/Delete operations, the Presenter should have access to the current selected item.
  • Add, Edit, and Delete Commands: Views use these commands to make it possible for the user to do create, edit and delete operations

Here is the code of above properties in the class.

public class ListPresenter<T, VIEW> : PresenterBase

where T : class

where VIEW : ViewBase

{

#region Properties

public virtual string Title

{

get

{

return title;

}

protected set

{

if (title != value)

{

title = value;

notifyPropertyChanged("Title");

}

}

}

private string title;

public ObservableCollection<T> Items

{

get

{

return items;

}

set

{

if (items != value)

{

items = value;

notifyPropertyChanged("Items");

}

}

}

private ObservableCollection<T> items;

public VIEW View { get; set; }

public IUnityContainer container;

public T SelectedItem

{

get

{

return selectedItem;

}

set

{

if (selectedItem != value)

{

selectedItem = value;

notifyPropertyChanged("SelectedItem");

}

}

}

private T selectedItem;

public ICommand EditCommand { get; set; }

public ICommand ViewCommand { get; set; }

public ICommand DeleteCommand { get; set; }

public ICommand AddCommand { get; set; }

public IDataService<T> Service { get; set; }

public IEventAggregator EventAggregator { get; set; }

protected string OnDeleteString;

#endregion

}

As you may notice, Items and SelectedItem property notify their changes using the notifyPropertyChanged. Notifying the changes is essential in order to announce the changes of data to Views.

Now, consider the things that the ListBasePresenter has implemented. The first thing that the presenter should do is initializing. In the initialize process, the Presenter resolves its members, its views and setting the DataContext of its views. Here is the initializing code.

public class ListPresenter<T, VIEW> : PresenterBase

where T : class

where VIEW : ViewBase

{

public ListPresenter(IUnityContainer container)

{

this.container = container;

Service = container.Resolve<IDataService<T>>();

View = container.Resolve<VIEW>();

EventAggregator = container.Resolve<IEventAggregator>();

EditCommand = new DelegateCommand<object>(editComandHandler);

AddCommand = new DelegateCommand<object>(addComandHandler);

ViewCommand = new DelegateCommand<object>(viewComandHandler);

DeleteCommand = new DelegateCommand<object>(deleteCommandHandler);

EventAggregator.GetEvent<CompositePresentationEvent<OnItemAddedEventArgs<T>>>().Subscribe(OnNewItemRecevied);

LoadItems();

View.Model = this;

}

}

One of the main members that the Presenter should resolve in the initializing phase is IDataService<T>. The presenter uses the IDataService<T> to load/save the data.

The other thing that a ListBasePresenter should do is loading the data. The loading of data has been done in the LoadItems method. Here is the code of LoadItems.

protected virtual void LoadItems()

{

List<T> list;

Service.Fill(out list

);

Items = new ObservableCollection<T>(list);

}

Whenever user fires the add/edit commands, the presenter should publish an event so that the ItemBasePresenter<T> can do the appropriate reactions. Here is the code of publishing the results.

protected virtual void editComandHandler(object parameter)

{

if (SelectedItem == null)

{

return;

}

// Call to edit data

EventAggregator.GetEvent<CompositePresentationEvent<EditItemEventArgs<T>>>().Publish(new EditItemEventArgs<T>(SelectedItem));

}

protected virtual void addComandHandler(object parameter)

{

T newData = (T)Activator.CreateInstance(typeof(T));

EventAggregator.GetEvent<CompositePresentationEvent<AddItemEventArgs<T>>>().Publish(new AddItemEventArgs<T>(newData));

}

protected void OnNewItemRecevied(OnItemAddedEventArgs<T> e)

{

if (!Items.Contains(e.Value))

{

Items.Add(e.Value);

SelectedItem = e.Value;

}

}

protected virtual void viewComandHandler(object parameter)

{

if (SelectedItem == null)

{

return;

}

// Call to edit data

EventAggregator.GetEvent<CompositePresentationEvent<ViewItemEventArgs<T>>>().Publish(new ViewItemEventArgs<T>(SelectedItem));

}

protected virtual void deleteCommandHandler(object parameter)

{

if (MessageBox.Show(OnDeleteString, "Warning", MessageBoxButton.YesNo, MessageBoxImage.Warning) == MessageBoxResult.Yes)

{

Service.Delete(SelectedItem);

int index = Items.IndexOf(SelectedItem);

Items.Remove(SelectedItem);

if (index > 0)

{

SelectedItem = Items[index - 1];

}

else

{

SelectedItem = null;

}

}

}

ItemBasePresenter<T>

Any presenter that is designed to add a new entity, or edit the current entity, or display the detail of current item should inherit from this base class. The class provides the following functionalities.

Listening for add, edit or view events

The ListPresenters fires the CompositePresentationEvents to announce their corresponding ItemPresenters about the adding or editing data. On the other hand, ItemBasePresenters listen to those events to do the corresponding reaction. The main advantage of using CompositePresentationEvent for communication among Presenters is the fact that the Presenters do not need to have access to each other. It is a major advantage that exists throughout the Composite pattern. Whenever, an ItemPresenter receives such event, it loads the item and displays its View. Here is the code of registering the methods of the class to listen to the CompositePresentationEvents.

EventAggregator = container.Resolve<IEventAggregator>();

EventAggregator.GetEvent<CompositePresentationEvent<EditItemEventArgs<T>>>().Subscribe(this.demandToEditItem);

EventAggregator.GetEvent<CompositePresentationEvent<AddItemEventArgs<T>>>().Subscribe(this.demandToAddItem);

EventAggregator.GetEvent<CompositePresentationEvent<ViewItemEventArgs<T>>>().Subscribe(this.demandToViewItem);

Here is the code of the demandToEditItem method.

protected virtual void demandToEditItem(EditItemEventArgs<T> e)

{

this.Mode = ViewMode.Edit;

this.Item = e.Value;

loadData();

PopulateForEdit();

IRegion region = regionManager.Regions[RegionName];

if (!region.Views.Contains(View))

{

region.Add(View);

}

region.Activate(View);

}

In the first line of the method, it sets the Mode to Edit. The Mode enumeration specifies the operation that the Presenter does. Here are the members of the enumeration.

public enum ViewMode { Add, Edit, View}

In the next lines, the method loads the data and populates the class properties. Finally it adds its View to the Region of the shell and activates it. For more information about the Regions and RegionManger, visits here. Here is the code of demandToAddItem

protected virtual void demandToAddItem(AddItemEventArgs<T> e)

{

this.Item = e.Value;

this.Mode = ViewMode.Add;

this.Item = e.Value;

PopulateForAdd();

IRegion region = regionManager.Regions[RegionName];

if (!region.Views.Contains(View))

{

region.Add(View);

}

region.Activate(View);

}

Saving Data

The saving data is a little complicated. First of all, the validation of the entity should be checked. Second, the any special process that should be done on the data before saving should be done. The next step is saving the data and finally publishing the changes. Here is the whole code of the Presenter.

public class ItemPresenter<T, VIEW> : PresenterBase

where T : class

where VIEW : ViewBase

{

public virtual string Name { get; set; }

public virtual string RegionName { get; set; }

public virtual string Title

{

get

{

return title;

}

protected set

{

if (title != value)

{

title = value;

notifyPropertyChanged("Title");

}

}

}

private string title;

public T Item

{

get

{

return item;

}

set

{

if (item != value)

{

item = value;

notifyPropertyChanged("Item");

}

}

}

private T item;

public IEventAggregator EventAggregator { get; set; }

public IDataService<T> Service { get; set; }

public ViewMode Mode { get; set; }

public VIEW View { get; set; }

protected IRegionManager regionManager;

protected virtual void SetData()

{

}

protected virtual void PopulateForAdd()

{

Title = String.Format("Add {0}", Name);

}

protected virtual void PopulateForEdit()

{

Title = String.Format("Edit {0}", Name);

}

protected virtual void PopulateForView()

{

Title = String.Format("View {0}", Name);

}

protected virtual bool isValid()

{

return true;

}

protected virtual void cancel()

{

}

protected virtual void save()

{

if (Mode == ViewMode.Add)

{

Service.Add(Item);

}

else if (Mode == ViewMode.Edit)

{

Service.Edit(Item);

}

}

protected virtual void close()

{

this.ClearErrors();

regionManager.Regions[RegionName].Deactivate(View);

}

protected virtual void loadData()

{

}

protected virtual void loadRelatedData()

{

}

protected virtual void demandToAddItem(AddItemEventArgs<T> e)

{

this.Item = e.Value;

this.Mode = ViewMode.Add;

this.Item = e.Value;

PopulateForAdd();

IRegion region = regionManager.Regions[RegionName];

if (!region.Views.Contains(View))

{

region.Add(View);

}

region.Activate(View);

}

protected virtual void demandToEditItem(EditItemEventArgs<T> e)

{

this.Mode = ViewMode.Edit;

this.Item = e.Value;

loadData();

PopulateForEdit();

IRegion region = regionManager.Regions[RegionName];

if (!region.Views.Contains(View))

{

region.Add(View);

}

region.Activate(View);

}

protected virtual void demandToViewItem(ViewItemEventArgs<T> e)

{

this.Mode = ViewMode.View;

this.Item = e.Value;

loadData();

PopulateForView();

}

public ItemPresenter(IUnityContainer container, IRegionManager regionManager)

{

this.regionManager = regionManager;

SaveCommand = new DelegateCommand<object>(saveCommandHandler);

CancelCommand = new DelegateCommand<object>(cancelCommandHandler);

Service = container.Resolve<IDataService<T>>();

EventAggregator = container.Resolve<IEventAggregator>();

EventAggregator.GetEvent<CompositePresentationEvent<EditItemEventArgs<T>>>().Subscribe(this.demandToEditItem);

EventAggregator.GetEvent<CompositePresentationEvent<AddItemEventArgs<T>>>().Subscribe(this.demandToAddItem);

EventAggregator.GetEvent<CompositePresentationEvent<ViewItemEventArgs<T>>>().Subscribe(this.demandToViewItem);

loadRelatedData();

View = container.Resolve<VIEW>();

View.Model = this;

}

public ICommand SaveCommand { get; set; }

public ICommand CancelCommand { get; set; }

protected void saveCommandHandler(object parameter)

{

if (isValid())

{

SetData();

save();

close();

if (Mode == ViewMode.Add)

{

EventAggregator.GetEvent<CompositePresentationEvent<OnItemAddedEventArgs<T>>>().Publish(new OnItemAddedEventArgs<T>(Item));

}

else if (Mode == ViewMode.Edit)

{

EventAggregator.GetEvent<CompositePresentationEvent<OnItemEditedEventArgs<T>>>().Publish(new OnItemEditedEventArgs<T>(Item));

}

}

}

protected void cancelCommandHandler(object parameter)

{

close();

}

}

IDataService<T>

Presenters use the IDataService<T> to load/save data. The IDataService<T> provides the basic CRUD operations on the entities of type T. Here is the code of IDataService<T> interface.

public interface IDataService<T> where T:class

{

void Add(T t);

void Edit(T t);

void Delete(T t);

void Fill(out List<T> list);

}

Using the Code

The above base classes make the code of ListPresenters very clean and simple. Here is the code of the AddressesPresenter that is responsible for displaying the list of addresses. The View associated with the Presenter is of type IAddressView.

public class AddressesPresenter : ListPresenter<Address, IAddressesView>, IAddressesPresenter

{

public AddressesPresenter(IUnityContainer container)

: base(container)

{

Title = "Addresses";

OnDeleteString = "Are you sure you want to delete the selected Address";

}

}

As can be seen, there is just one constructor with two lines of code in the Presenter. In the first line of the constructor the Title of the Presenter has been set. This title will be displayed as the title of the View. In the second line, the OnDeleteString has been set. OnDeleteString is a confirm message that will be displayed to user to confirm the delete action whenever user selects the delete operation.

Here is the code of AddressPresenter that is responsible for adding/editing the address entities.

public class AddressPresenter : ItemPresenter<Address, IAddressView>, IAddressPresenter

{

public AddressPresenter(IUnityContainer container, IRegionManager regionManager)

: base(container, regionManager)

{

Name = "Address";

RegionName = RegionNames.PopupRegion;

}

protected override void PopulateForAdd()

{

base.PopulateForAdd();

Item.rowguid = Guid.NewGuid();

}

protected override void SetData()

{

base.SetData();

Item.ModifiedDate = DateTime.Now;

}

protected override bool isValid()

{

if (Item.City == null)

{

Item.AddError("City", "Error");

return false;

}

if (Item.City != null && Item.City.Length > 20 || Item.City.Trim().Length == 0)

{

Item.AddError("City", "Error");

return false;

}

if (Item.PostalCode== null)

{

Item.AddError("PostalCode", "Error");

return false;

}

if (Item.PostalCode != null && Item.PostalCode.Length > 50 || Item.PostalCode.Trim().Length == 0)

{

Item.AddError("PostalCode", "Error");

return false;

}

if (Item.AddressLine1 == null)

{

Item.AddError("AddressLine1", "Error");

return false;

}

if (Item.AddressLine1 != null && Item.AddressLine1.Length > 50 || Item.AddressLine1.Trim().Length == 0)

{

Item.AddError("AddressLine1", "Error");

return false;

}

if (Item.CountryRegion == null)

{

Item.AddError("CountryRegion", "Error");

return false;

}

if (Item.CountryRegion != null && Item.CountryRegion.Length > 50 || Item.CountryRegion.Trim().Length == 0)

{

Item.AddError("CountryRegion", "Error");

return false;

}

if (Item.StateProvince == null)

{

Item.AddError("StateProvince", "Error");

return false;

}

if (Item.StateProvince != null && Item.StateProvince.Length > 50 || Item.StateProvince.Trim().Length == 0)

{

Item.AddError("StateProvince", "Error");

return false;

}

return true;

}

Actually, classes that inherit from the ItemPresenters should override the PopulateForAdd, IsValid, and SetData methods. A complete demo can be downloaded here You need VS 2008 and SQL EXPRESS to run the demo.
As you may notice, using the library makes the code of Presenters clean and simple. In the next paper, I will extend the library to cover more scenarios.

By Siyamand Ayubi   Popularity  (3379 Views)