65.9K
CodeProject 正在变化。 阅读更多。
Home

使用适配器模式显示清单, 以最大限度地减少所需的 ViewModel 数量

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (7投票s)

2015年3月18日

CPOL

7分钟阅读

viewsIcon

12921

downloadIcon

141

使用适配器模式显示复选框列表,以最大限度地减少所需的视图模型数量。

引言

最近我想显示多个复选框项的 listview,这些项属于不同的类型。我越是思考这个问题,就越不想为每种类型创建一个单独的DataTemplate和/或特定的视图模型,尤其是我要选择的类型都是“简单”类型,通常不会被包装成视图模型。例如:

事件类型是一个枚举,注册的源是字符串。

复选框列表项DataTemplate中元素的绑定路径将在 XAML 中定义,因此我需要一种方法将任何任意类型的属性映射到预期的DataTemplate

详细说明

数据模板

可选项目的模板非常简单。它期望项具有IsSelected属性和DisplayString属性。

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

... 并将呈现为 ...

问题在于,字符串、枚举以及您可能想要选择的几乎任何其他类型都没有现成的IsSelectedDisplayString属性。那么该怎么办?

当然,您可以在应用程序中的所有模型中添加IsSelected属性和DisplayString属性,然后就不用管它了。但是,如果您不知道哪些模型最终会出现在选择列表视图中呢?您是否为了以防万一而在每个类中都添加IsSelectedDisplayString?这有点繁琐。

再说,如果您想显示枚举的选择列表?或者一个第三方库中密封类型的列表,而您没有该库的源代码?

我们需要一个适配器或包装器。一个可以将任何任意类型转换为可以与CheckedListItem数据模板通信的东西。

适配器

SelectableItemVM是执行繁重工作的类。它具有数据模板所需的IsSelectedDisplayString属性,以及多种将被包装类型的一个或多个属性转换为DisplayString的方法。

BaseVM不一定是必需的,但它通过为从中派生的任何视图模型实现INotifyPropertyChanged接口,并为派生的视图模型提供现成的触发属性更改事件的方法,从而节省了一些输入。

SelectableItemVM<T>

接受类型T的实例,并将选定的字符串属性或该类型的属性映射到DisplayString,并且通过实现ISelectable接口,为我们提供了IsSelected属性。

方法 范围 Returns 注释
SelectableItemVM(T Item) Public   构造函数:为类型 T 创建 VM 实例,该实例使用类型的默认ToString方法返回列表项的复选框文本。
SelectableItemVM(T Item, string PropertyName) Public   构造函数:为类型T创建 VM 实例,该实例使用该类型的命名属性来返回列表项的复选框文本。命名属性必须是返回字符串的类型的公共实例属性,并且应用程序必须以足够的信任运行,才能使用反射访问类型的属性。
SelectableItemVM(T Item, GetDisplayText getTextMethod) Public   构造函数:为类型T创建 VM 实例,该实例使用提供的委托函数返回列表项的复选框文本。用于没有现有文本属性的类型,或者需要组合类型的某些属性,或者应用程序没有以足够的信任运行来通过反射访问类型的属性。
IsSelected Public 布尔值 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 字符串 Property: The name of the public instance property that returns the text representation of the type T represented by the VM.
DisplayString Public 字符串 Property: Returns the value of the property named by DisplayPropertyName If DisplayPropertyName 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 字符串 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 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.

示例

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(source);
        registeredSources.AddRange(qrySources.ToList());
      }
      return registeredSources;
    }
  }
  private List<SelectableItemVM<String>> registeredSources;
  public const string RegisteredSourcesProperty = @"RegisteredSources";

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. :)

历史

日期 备注
Mar 2015 First cut for publication.
© . All rights reserved.