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

WPF:改进了选择

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (44投票s)

2009年7月4日

CPOL

7分钟阅读

viewsIcon

150154

downloadIcon

1330

为用户提供更好的选择选项。

引言

我离开了一段时间,在构思写一本书;对于所有在我的博客上留下鼓励话语的朋友们,我非常感谢。这对我意义重大。不幸的是,出版商非常狭隘,看不到更大的图景,也看不到我们的书会是什么样子。所以书的项目就此搁置。

因此,这篇文章是我重返 CodeProject usual article tirade 的第一篇文章,这里我感觉才是我的真正归宿。

这是一篇短文,但我可以保证,接下来会有一系列非常重要的文章,内容是我认为迄今为止我最好的、最有用的作品。即将发布的文章系列将围绕一个用于处理 WPF 的 MVVM 框架展开;它实际上解决了我在使用 WPF、测试和 MVVM 模式时遇到的所有问题/不足,所以请持续关注这个系列。

但我们现在所处的位置就是这里,这就是这篇文章,那么这篇文章到底做了什么呢?嗯,它相当简单。我们在 UI 中大量使用组合框 (comboboxes) 来让用户选择值并显示选定的值,这很好,所以我们有类似这样的东西

这非常有效,前提是你的数据量相对较少且复杂度不高。请记住,在 WinForms 和 WPF 中,你可以将任何对象的列表作为组合框的数据源,所以将复杂类的列表作为 ItemsSource 并非不可想象。上面的选择/显示方法可能无法满足需求,因此可能需要一些更高级的功能。

如果我们能将当前选定的项目保持为一个简单的字符串,并允许用户看到一个 DataGridListView 来从中选择当前项目,那不是很好吗?

幸运的是,WPF 非常强大,我们可以做到这一点。

这是我提出的方案

我们看到的是,当前项目只是选中整个对象的简短属性表示,但当用户想要进行新选择时,他们会在一个合适的显示容器中看到整个对象;在我的示例中,我使用的是标准的 (Styled) WPF ListView,但你可以使用任何你喜欢的东西。

这一切是如何工作的?如果你想知道,请继续阅读。

工作原理

首先要理解的是,组合框 (combobox) 用来选择的是什么。在这个简单的演示代码(已附带)中,我为 ComboxBox.ItemsSource 使用了一个 ObservableCollection<Person>,但这可以是任何 IEnumerable,所以像 List<Person> 这样也可以。

我通过 ViewModel 中的绑定来设置 ComboBox.ItemSource,该 ViewModel 用于演示代码的 Window 的 DataContext。这是完整的 ViewModel 代码。虽然这个 ViewModel 代码实际上并不重要,你只需要了解的是 ComboxBox.ItemsSource 被绑定到 ViewModel 的 People 属性,而该属性是一个 ObservableCollection<Person>

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Windows.Data;

namespace WpfApplication1
{
    public class PeopleViewModel : INotifyPropertyChanged
    {
        private Person currentPerson = null;
        private ObservableCollection<Person> people = 
            new ObservableCollection<Person>();
        private ICollectionView peopleCV = null;

        public PeopleViewModel()
        {
            this.people.Add(new Person 
                { 
                    FirstName = "sacha",
                    MiddleName = "",
                    LastName = "Barber1"
                });
            this.people.Add(new Person
            {
                FirstName = "leanne",
                MiddleName = "riddley",
                LastName = "rymes"
            });
            peopleCV = CollectionViewSource.GetDefaultView(people);
            peopleCV.MoveCurrentToPosition(-1);
        }

        public Person CurrentPerson
        {
            get { return currentPerson; }
            set
            {
                if (currentPerson != value)
                {
                    currentPerson = value;
                    NotifyChanged("CurrentPerson");
                }
                else
                    return;
            }
        }

        public ObservableCollection<Person> People
        {
            get { return people; }
            set
            {
                if (people != value)
                {
                    people = value;
                    peopleCV = CollectionViewSource.GetDefaultView(people);
                    peopleCV.MoveCurrentToPosition(-1);
                    NotifyChanged("People");
                }
                else
                    return;
            }
        }

        #region INotifyPropertyChanged Implementation

        /// <summary>
        /// Occurs when any properties are changed on this object.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;


        /// <summary>
        /// A helper method that raises the PropertyChanged event for a property.
        /// </summary>
        /// <param name="propertyNames">The names
        ///       of the properties that changed.</param>
        protected virtual void NotifyChanged(params string[] propertyNames)
        {
            foreach (string name in propertyNames)
            {
                OnPropertyChanged(new PropertyChangedEventArgs(name));
            }
        }

        /// <summary>
        /// Raises the PropertyChanged event.
        /// </summary>
        /// <param name="e">Event arguments.</param>
        protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged(this, e);
            }
        }

        #endregion
    }
}

这是一个 Person 对象实际的样子

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;

namespace WpfApplication1
{
    public class Person : INotifyPropertyChanged
    {
        private String firstName = String.Empty;
        private String middleName = String.Empty;
        private String lastName = String.Empty;

        public String FormattedName
        {
            get
            {
                return FirstName.Substring(0, 1) + "." +
                       LastName;
            }
        }

        public String FirstName
        {
            get { return firstName; }
            set
            {
                if (firstName != value)
                {
                    firstName = value;
                    NotifyChanged("FirstName");
                }
                else
                    return;
            }
        }

        public String MiddleName
        {
            get { return middleName; }
            set
            {
                if (middleName != value)
                {
                    middleName = value;
                    NotifyChanged("MiddleName");
                }
                else
                    return;
            }
        }

        public String LastName
        {
            get { return lastName; }
            set
            {
                if (lastName != value)
                {
                    lastName = value;
                    NotifyChanged("LastName");
                }
                else
                    return;
            }
        }

        #region INotifyPropertyChanged Implementation

        /// <summary>
        /// Occurs when any properties are changed on this object.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;


        /// <summary>
        /// A helper method that raises the PropertyChanged event for a property.
        /// </summary>
        /// <param name="propertyNames">
        ///         The names of the properties that changed.</param>
        protected virtual void NotifyChanged(params string[] propertyNames)
        {
            foreach (string name in propertyNames)
            {
                OnPropertyChanged(new PropertyChangedEventArgs(name));
            }
        }

        /// <summary>
        /// Raises the PropertyChanged event.
        /// </summary>
        /// <param name="e">Event arguments.</param>
        protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged(this, e);
            }
        }

        #endregion
    }
}

这里有一个重要的点需要注意,我在 Person 类上有一个 FormattedName 属性,它代表了整个对象的简短显示表示。我正是使用这个 FormattedName 属性来显示 ComboBox 中当前选定的项目;基本上,它只是对选定对象的简短表示。

到目前为止,我们已经将 ComboBox 绑定到一个 ObservableCollection<Person>;很好,那么其余部分是如何工作的呢?嗯,要深入了解,我们需要理解 ComboBox 的 ControlTemplate 是如何工作的。ControlTemplate 中有一个弹出窗口,用于容纳项目,还有一个 Content 属性,用于显示当前选定的项目。了解了这一点,我们就可以开始让 ComboBox 按照我们想要的方式工作了。

让 ComboBox 显示 Grid 作为其 ItemPresenter

第一步是让它显示一个 DataGridListView 作为其项目。我们怎么做到这一点呢?我提出的解决方案是“作弊”。我们像往常一样使用 ComboBox,但我们将一个 DataGridListView 放在它的第一个项目位置,并给它一个负的 Margin,这样我们就永远看不到 ComboBox 项目周围的标准 ComboBox 选择颜色(它实际上是我们的 DataGridListView)。

这是我正在做的事情

<local:ComboBoxEx >

    .......
    .......
    .......
    .......

    <local:ComboBoxEx.Items>
        <ComboBoxItem>

            <ListView AlternationCount="0"
                      Margin="-5,-2,-5,-2"
                      Background="White"
                      Height="200"
                      ItemsSource="{Binding Path=People}"
                      SelectedValue="{Binding Path=CurrentPerson}"
                      ItemContainerStyle="{DynamicResource ListItemStyle}"
                      BorderBrush="Transparent"
                      VerticalAlignment="Stretch"
                      HorizontalAlignment="Stretch"
                      IsSynchronizedWithCurrentItem="True"
                      local:SortableList.IsGridSortable="True"
                      FontSize="12"
                      SelectionMode="Single">
                <ListView.Resources>

                    <Style x:Key="ListItemStyle"
                           TargetType="{x:Type ListViewItem}">
                        <Setter Property="Template"
                                Value="{StaticResource EntityListViewItemTemplate}" />
                        <Setter Property="HorizontalContentAlignment"
                                Value="Left" />

                    </Style>

                </ListView.Resources>

                <ListView.View>
                    <GridView ColumnHeaderContainerStyle="{StaticResource 
                            GridViewColumnHeaderStyle}">
                        <GridViewColumn Header="FirstName"
                                        DisplayMemberBinding="{Binding FirstName}" />
                        <GridViewColumn Header="Middle Name"
                                        DisplayMemberBinding="{Binding MiddleName}" />
                        <GridViewColumn Header="Last Name"
                                        DisplayMemberBinding="{Binding LastName}" />
                    </GridView>
                </ListView.View>
            </ListView>
        </ComboBoxItem>
    </local:ComboBoxEx.Items>
</local:ComboBoxEx>

眼尖的读者会注意到,我没有使用标准的 WPF ComboBox,而是使用了 ComboBoxEx;不用担心,我稍后会讲到。现在,请理解我们正在使用标准的 WPF ComboBox.Items 集合,并像标准 WPF ComboBox 一样提供一个 ComboBoxItem。碰巧的是,ComboBox 只有一个项目,那就是我们的 DataGridListView

这就解释了我们如何让 DataGridListView 显示出来。那么选中的项目呢?既然当前项目实际上是 DataGridListView,那么选中的项目肯定也是 DataGridListView。是的,在正常情况下,确实是这样,而且看起来会很奇怪;它会是这样的

这实际上并不是我们想要的。我们该如何解决呢?这确实需要对 ComboBox 的 ControlTemplate 工作原理有更多的了解。当你查看它时,你会发现有一个 ContentPresenter 用于表示当前选定项目的 Content,它默认使用 TemplateBinding 来绑定到 Content。这解释了我们上面看到的情况;Content 实际上是一个 DataGridListView。这很有趣,所以也许,如果我们使用另一个属性,我们可以让它显示别的东西。

<ContentPresenter x:Name="item"
      SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
      HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
      Margin="{TemplateBinding Padding}"
      VerticalAlignment="Center"
      Grid.Column="1"
      Content="{TemplateBinding SelectionBoxItem}"
      ContentTemplate="{TemplateBinding Content}"
      ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}" />

更改选定项目的内容

当我第一次研究这个问题时,我希望能够将 ContentTemplate="{TemplateBinding Content}" 替换为一个附加的 DP,但这似乎不起作用,因为 DP 实际上并未被视为 TemplateBinding 标记扩展的标准可用属性。我当时想,好吧,我们只能子类化 ComboBox 并添加一个我们想要用于 Content 的属性,并在 ControlTemplate 中使用它。

这正是我所做的;这是 ComboBoxEx 的完整代码

public class ComboBoxEx : ComboBox
{
    #region SelectedTemplateOverride

    /// <summary>
    /// SelectedTemplateOverride Dependency Property
    /// </summary>
    public static readonly DependencyProperty SelectedTemplateOverrideProperty =
        DependencyProperty.Register("SelectedTemplateOverride", 
        typeof(DataTemplate), typeof(ComboBoxEx),
            new FrameworkPropertyMetadata((DataTemplate)null));

    /// <summary>
    /// Gets or sets the SelectedTemplateOverride property.
    /// </summary>
    public DataTemplate SelectedTemplateOverride
    {
        get { return (DataTemplate)GetValue(SelectedTemplateOverrideProperty); }
        set { SetValue(SelectedTemplateOverrideProperty, value); }
    }

    #endregion
}

正如我所说,如果能使用附加 DP 就好了,但没办法。

因此,有了这个 ComboBoxEx 类,我们就可以更改应用于它的标准 ControlTemplate,以使用我们新的 SelectedTemplateOverride DP。让我们在 ComboBoxEx ControlTemplate 的相关部分看看。

<ContentPresenter x:Name="item"
          SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
          HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
          Margin="{TemplateBinding Padding}"
          VerticalAlignment="Center"
          Grid.Column="1"
          Content="{TemplateBinding SelectionBoxItem}"
          ContentTemplate="{TemplateBinding SelectedTemplateOverride}"
          ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}" />

请注意,我们不再使用 ContentTemplate="{TemplateBinding Content}",而是使用 ContentTemplate="{TemplateBinding SelectedTemplateOverride}",这是我们在 ComboBoxEx 类中引入的新 DP。

所以,我们现在需要做的就是往 SelectedTemplateOverride DP 中放入一些东西;在 XAML 中使用 ComboBoxEx 对象实际实例的地方可以做到这一点。

这是相关的 XAML 片段

<local:ComboBoxEx.SelectedTemplateOverride>
    <DataTemplate>
        <Label DataContext="{Binding ElementName=theView, Path=DataContext}"
               Content="{Binding Path=CurrentPerson.FormattedName, 
                    UpdateSourceTrigger=PropertyChanged, Mode=OneWay}"
               VerticalContentAlignment="Center"
               Padding="0"
               Margin="2,0,0,0" />
    </DataTemplate>
</local:ComboBoxEx.SelectedTemplateOverride>

请注意,它只是一个 DataTemplate,因为 SelectedTemplateOverride DP 的类型就是如此。另外两点值得注意

  1. 我们正在使用我们特殊的、简化的 FormattedName 属性,我们在前面讨论演示 (Person) 类时已经看到了。
  2. 我必须从某个地方获取 DataContext 来用于标签,这样绑定才能工作,所以我从宿主 Window 获取它,因为整个 ViewModel 已经设置为它的 DataContext。ViewModel 实际上知道当前的项目是哪个,这是通过 ICollectionViewIsSynchronizedWithCurrentItem="True" 的魔力实现的,而后者在演示代码的 ListView (CombBox 的单个项目) 上设置了。我没有讨论 ICollectionViewIsSynchronizedWithCurrentItem="True",它们所做的只是保持 ListView 中的选择与 ViewModel 中的 ICollectionView 同步,这让我可以从 DataContext(从 Window,因为它有 ViewModel 作为 DataContext)中获取当前选定的项目。

所以,随着这个拼图的最后一块解决,我们最终得到的结果是,选定项目是 ListView 中当前选定的 Person,它被用作 CombBox 中选定项目的 Contemt

奖励

附带的代码还演示了如何使用一个名为 SortableList 的附加 DP 来对 ListView 列进行排序,你像这样将其设置在你的 ListView

<ListView local:SortableList.IsGridSortable="True" SelectionMode="Single">

你可以深入研究 SortableList 来了解它的工作原理。

结束

总之,我希望一切都清楚了。如果你喜欢,可以留下一个投票和评论,那将很好。

享受。

© . All rights reserved.