Display Checklists Using The Adaptor Pattern To Minimise The Number Of View-models ...

:

Introduction

Recently I wanted to be able display multiple listviews of checkbox items for different types and the more I thought about it, the less I wanted to create a separate DataTemplate and/or a specific view-model for each type especially as the types to be selected were "simple" types and wouldn't normally be wrapped in a view-model. For example:

Event Type is an enumeration and registered sources are strings.

The binding paths for the elements in the checklist item DataTemplate are going to be defined in XAML so I need a way to map the properties of any arbitrary type to the intended DataTemplate.

Details

DataTemplate

The data template for a selectable item is very simple. It expects items with an IsSelected property and a DisplayString property.

<datatemplate x:key="CheckedListItem">
  <stackpanel orientation="Horizontal">
    <checkbox margin="5,0,0,0"
              ischecked="{Binding Path=IsSelected,
                          Mode=TwoWay,
                          UpdateSourceTrigger=PropertyChanged}" />
    <textblock margin="5,0,0,0"
               text="{Binding Path=DisplayString}" />
  </stackpanel>
</datatemplate>

... and will render as ...

The problem is that strings, enumerations and just about any other type you might want to select don't come ready-fitted with either IsSelected or DisplayString properties. So what to do?

You could of course add an IsSelected property and a DisplayString property to all the models in your application and leave it at that. But what if you don't know which of your models are going to end up appearing in selection listviews? Do you add IsSelected and DisplayString to every single class just in case? Bit tedious that.

Then again; what if you want to display a selection list of enumerations? Or a list of a sealed type in a third party library for which you do not have the source?

We need an Adaptor or Wrapper. Something that converts any arbitrary type into something that can talk to CheckedListItem data template.

Adaptor

SelectableItemVM is the class that does the donkey work. It has the IsSelected and DisplayString properties required by the data template and a number of ways of turning one or more properties of the wrapped type into a DisplayString.

BaseVM isn't strictly necessary, but it does save some typing when setting up view-models by implementing the INotifyPropertyChanged interface for any view-model derived from it and providing derived view-models with ready-rolled methods to fire property changed events.

SelectableItemVM<T>

Takes an instance of type T and maps a selected string property or properties of the type to DisplayString and by implementing the ISelectable interface provides us with the IsSelected property.

Method Scope Returns Comment
SelectableItemVM(T Item) Public   Constructor: Creates an instance of the VM for type T that uses the type's default ToString method to return the list item's checkbox text.
SelectableItemVM(T Item, string PropertyName) Public   Constructor: Creates an instance of the VM for type T that uses the type's named property to return the the list item's checkbox text. The named property must be a public instance property of the type returning string and the application must be running with sufficient trust to be able to access the type's properties using reflection.
SelectableItemVM(T Item, GetDisplayText getTextMethod) Public   Constructor: Creates an instance of the VM for type T that uses the supplied delegate function to return the the list item's checkbox text. For use with types that do not have an existing text property or where a some custom combination of the type's properties is required or where the application is not running with sufficient trust to access the type's properties using reflection.
IsSelected Public Boolean Property: This is bound to the checkbox in the CheckedListItem DataTemplate and will be set true or false according to the state of the checkbox.
Item<T> Public <T> Property: The item of type T represented by the VM.
DisplayPropertyName Public string Property: The name of the public instance property that returns the text representation of the type T represented by the VM.
DisplayString Public string Property: Returns the value of the property named by DisplayPropertyName If DisplayPropertyName or is not set or is not the name of a public instance property of the type or no delegate is specified the value of the type T's ToString method is returned.
ToString Public string Function: Returns the value of DisplayString. Allows default bindings that use a type T's string description.

ISelectableItem

Is, in its entirety...

/// <summary>
/// Any item that can be selected.
/// </summary>
interface ISelectable {
    Boolean IsSelected {get; set;}
}

ISelectable is not absolutely necessary, but by creating the interface we make it easier to walk lists of selectable items without having to know their underlying types. This is shown in more detail later on.

Examples

1. A simple list of String

For a view model, MainVM, that has some property returning a list of strings one or more of which can be selected.

Because the ToString method for String returns a... , go on guess, we don't need to provide any additional information about how to work out DisplayString.

/// <summary>
/// Which event sources are available for selection?
/// </summary>
/// <remarks>
/// A simple list of strings.
/// </remarks>
public List<SelectableItemVM<String>> RegisteredSources {
  get {
    if (registeredSources == null) {
      registeredSources = new List<SelectableItemVM<String>>();
      var qrySources = from string source in eventLog.AvailableSources
                       orderby source ascending
                       select new SelectableItemVM<string>(source);
      registeredSources.AddRange(qrySources.ToList());
    }
    return registeredSources;
  }
}
private List<SelectableItemVM<String>> registeredSources;
public const string RegisteredSourcesProperty = @"RegisteredSources";</string>

To set the view binding in code:

// Make sure we have the correct data template for the list items.
filterSources.ItemTemplate = (DataTemplate)FindResource(@"CheckedListItem");
filterSources.SetBinding(ListView.ItemsSourceProperty, MainVM.RegisteredSourcesProperty);

Having done this it becomes very easy to act on the selected items in methods or commands in MainVM, for example..

private void actOnSelectedSources {

  var qrySelectedSources = from SelectableItemVM<string> source in RegisteredSources
                           where source.IsSelected
                           select source.Item;

  // Do something useful with the list of selected strings.
}

2. Models with a usable text property

For a view model, MainVM, that has some property returning a list of sales contacts one or more of which can be selected, where SalesContact may look something like...

class SalesContact {
  public string ContactName {get; set;}   // <--- We want to display this property.
  public string Telephone {get; set;}
  public string EmailAddress {get; set}
  public bool CreditWorthy {get; set;}
  :
  :
}

Because the SalesContact ToString method will return something like "SomeNamespace.SalesContact" we have to tell the adaptor what it should pipe through to its DisplayString property. For this example we'll specify "ContactName".

/// <summary>
/// Credit worthy sales contacts.
/// </summary>
public List<SelectableItemVM<SalesContact>> CreditWorthy {
  get {

    if (creditWorthy == null) {
      creditWorthy = new List<SelectableItemVM<SalesContact>>();

      var qryContacts = from SalesContact contact in GetSalesContacts()
                        where contact.CreditWorthy()
                        orderby contact.ContactName ascending
                        select new SelectableItemVM<SalesContact>(contact, "ContactName");

      creditWorthy.AddRange(qryContacts.ToList<SelectableItemVM<SalesContact>>());
    }

    return creditWorthy;
  }
}
private List<SelectableItemVM<SalesContact>> creditWorthy;
public const string CreditWorthyContactsProperty = @"CreditWorthy";

Setting the view binding in code is exactly as shown above for the list of strings.

contacts.ItemTemplate = (DataTemplate)FindResource(@"CheckedListItem");
contacts.SetBinding(ListView.ItemsSourceProperty, MainVM.CreditWorthyContactsProperty);

And as with the list of strings you have direct access to the selected item ...

private void actOnSelectedContacts {
  
  var qryCreditWorthy = from SelectableItemVM<SalesContact> prospect in CreditWorthy
                        where prospect.IsSelected
                        select prospect.Item;
    
  // Send a "personalised" e-mail...
  sendEnticingOffer(qryCreditWorthy.ToList<SalesContact>());
}

3. Models without a usable text property

There are three ways around this:

  • If you have access to the model's source code and can add a text property and want to do so then use as example 2 above.
  • Create a delegate method.
  • Create a derivation of SelectableItemVM.

Either of the latter two approaches is suitable for use when:

  • You don't want to start burdening your classes with properties that have nothing to do with what the class is modelling.
  • You don't have access to a third party type's source code.
  • You want to combine one or more text properties to create your description text.
  • The application isn't running with sufficient trust to use reflection to access properties in the type.

Of the two I would choose the delegate method approach. It keeps the number of view models down, which was the main reason for using an adaptor and it also means that should you choose to change the content/format of the text you want to display in checklists you only have to change it in one place.

The Delegate Solution
// Somewhere else in your project. Possibly in the class in question or perhaps
// some common library class.
public static string GetContactName(SalesContact contact) {
  return contact.ContactName;
}

And SelectableItemVM construction for SalesContact becomes ...

// Assuming we've put all our delegate methods in a class called Delegates
var qryContacts = from SalesContact contact in GetSalesContacts()
            where contact.CreditWorthy()
            orderby contact.ContactName ascending
            select new SelectableItemVM<SalesContact>(SalesContact, Delegates.GetContactName);

If you wanted to show both name and 'phone No. the delegate becomes:

public static string GetContactName(SalesContact contact) {
  return string.format("{0} - {1}") contact.ContactName, contact.Telephone) ;
}
The Derived VM class Solution

I can't see much point to doing this, but if you have some compelling reason to go this way feel free.

If you want to use SelectableItemVM and you don't want your colleagues doing this then seal the class and don't allow the override of DisplayString.

/// <summary>
/// A selectable event type.
/// </summary>
class EventTypeVM : SelectableItemVM<EventLogEntryType>  {
  public EventTypeVM(EventLogEntryType eventType) : base(eventType) {/*Nothing to do here.*/}

  /// <summary>
  /// Convert some of the camel cased enumerations to space separated words.
  /// </summary>
  public override string DisplayString {
    get {
      string legend = "Unknown";
      switch(Item) {
        case EventLogEntryType.SuccessAudit:
          legend = "Success Audit";
          break;
        case EventLogEntryType.FailureAudit:
          legend = "Failure Audit";
          break;
        default:
          legend = Item.ToString();
          break;
        }
      return legend;
   }
  }
}

Which can then be used as below ...

/// <summary>
/// The sort of events (error, information etc) to retrieve.
/// </summary>
public List<EventTypeVM> EventTypes {
  get {
    if (eventTypes == null) {
      eventTypes = new List<EventTypeVM>();
      var qryEventTypes = from EventLogEntryType eType in Enum.GetValues(typeof(EventLogEntryType))
                          orderby eType.ToString() ascending
                          select new EventTypeVM(eType);
      eventTypes.AddRange(qryEventTypes.ToList<EventTypeVM>());

    }
    return eventTypes;
  }
}
private List<EventTypeVM> eventTypes;
public const string EventTypesProperty = @"EventTypes";

This example uses a one-time assigment because the list is unchanging but if it were a volatile list you would set the binding as in the previous examples.

filterEventTypes.ItemTemplate = (DataTemplate)FindResource("CheckedListItem");
filterEventTypes.ItemsSource = ViewModel.EventTypes;

Points of Interest/Limitations

Only Intended for 'Passive' Use

If the MainVM wants to know what the selection states of one or more items are it has to ask as shown in example 1.

Use of Reflection - DisplayPropertyName

Although there are potential trust issues when using Reflection to access a named property it seems likely that in the majority of cases the ability to specify a named text property without going to the effort of providing a separate delegate method is too useful to ignore (read: I am a lazy so and so).

For more information see:

Reflection Security Issues

What's the point of ISelectable?

Good question. The solution started out without it, but I found that there were times when all I needed to was find the number of selected items, clear blocks or set blocks of selected items and where the underlying type was irrelevant. This sort of thing:

/// <summary>
/// Set all items to selected in the referenced control.
/// </summary>
/// <param name="CommandParameter">
/// A listview having a list of items that implement ISelectable.
/// </param>
private void CheckboxSet(object CommandParameter) {
  ListView control = (ListView)CommandParameter;
  var qryAllItems = from ISelectable item in control.ItemsSource
                    select item;
  foreach (ISelectable item in qryAllItems) {
    item.IsSelected = true;
  }
}

If you try and iterate over SelectableItemVM you have to know the type T used to instantiate SelectableItemVM which can be awkward in a general purpose method, especially if you are using a VM derived from SelectableItemVM.

Why haven't you shown XAML bindings?

Because I find life very much less troublesome specifying bindings in code. It also makes life very much easier for those who have to maintain stuff I write because they don't have to be XAML gurus to understand what's going on. More importantly; neither do I.

A Final Thought

Something for you to ponder. Considered as a breed, are view-models Adaptors or are they Facades? Discuss using only one side of the VDU and show your working. :)

History

Date Remarks
Mar 2015 First cut for publication.