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...
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
.
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:
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;
}
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;}
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"
.
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;
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
public static string GetContactName(SalesContact contact) {
return contact.ContactName;
}
And SelectableItemVM
construction for SalesContact
becomes ...
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
.
class EventTypeVM : SelectableItemVM<EventLogEntryType> {
public EventTypeVM(EventLogEntryType eventType) : base(eventType) {}
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 ...
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:
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. |