首页 > 解决方案 > 2 ObservableCollections 1 组合框 WPF

问题描述

我有一个 WPF 应用程序,它建立到另一台计算机的连接。在我的应用程序中,我有一个组合框,用户可以在其中输入计算机的主机名,然后连接到这台计算机。现在一旦建立连接,用户输入的主机名就会保存到绑定到组合框的 Observable Collection 中,因此下次他想连接到同一主机时,他可以直接从组合框中选择它。

我已经实现了一个收藏列表。这是一个单独的可观察集合,我也想绑定到同一个组合框,因此用户可以选择收藏项或历史项。

在组合框的下拉列表中,我想要 2 个带有标题的分组,如下所示:

    [Favorites]
         My Favourite Host | myfavhost.com
         My 2nd Fav | my2ndfav.com
         Secretly My Fav | secretlymyfav.com
    [History]
         hostioncevisited.com
         whyamihere.com
         thanksforhelping.com

现在我真的不知道该怎么做。有没有办法将多个项目源绑定到组合框,或者我必须在将两个可观察集合绑定到组合框之前合并它们?

这些是我的可观察收藏

public ObservableCollection<string> HistoryItems { get; set; } = new ObservableCollection<string>();

public static ObservableCollection<FavoriteItem> FavoriteItems { get; set; } = new ObservableCollection<FavoriteItem>();

这是我的 FavoriteItem 类

public class FavoriteItem : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private string hostName;
        private string description;

        public FavoriteItem(){}
        public FavoriteItem(string _hostName, string _description)
        {
            hostName = _hostName;
            description = _description;
        }

        public string Hostname
        {
            get { return hostName; }
            set
            {
                hostName = value;
                OnPropertyChanged("Hostname");
            }
        }

        public string Description
        {
            get { return description; }
            set
            {
                description = value;
                OnPropertyChanged("Description");
            }
        }

        protected void OnPropertyChanged(string name)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(name));
            }
        }

        public override string ToString()
        {
            return string.Format("{0} | {1}", description, hostName);
        }
    }

这是组合框的 XAML

XAML

<ComboBox Name="cbHostName" Style="{StaticResource ComboBoxLarge}" Text="{Binding HostName}" ItemsSource="{Binding HistoryItems}" 
                          MinWidth="300" MaxWidth="300" IsEditable="True" Margin="0,0,15,0" VerticalAlignment="Center" materialDesign:HintAssist.Hint="Computer, IP or HostProfileName"/>

标签: c#.netwpfcomboboxobservablecollection

解决方案


您可以使用CompositeCollection将多个集合绑定到同一个源。

这是一个例子

缺点是我认为在这种情况下不可能进行分组(至少不容易)。


另一种方法是只有一个实现相同接口的对象列表,并具有一些属性来区分项目的类型,例如:

public interface IHost : INotifyPropertyChanged
{
    string HostType { get; }
    string Hostname { get; set; }
    string DisplayText { get; set; }
}

public class HistoryItem : IHost
{
    public event PropertyChangedEventHandler PropertyChanged;

    public string HostType => "History";
    public string Hostname { get; set; }
    public string DisplayText => Hostname;
}

public class FavoriteItem : IHost
{
    public event PropertyChangedEventHandler PropertyChanged;

    public string HostType => "Favorites";
    public string Hostname { get; set; }
    public string Description { get; set; }
    public string DisplayText => Description == null ? Hostname : $"{Description} | {Hostname}";
    //other properties....
}

当我发现直接与ObservableCollection烦人一起工作时,我倾向于使用包装器(底部的代码)。它处理一些常见问题,例如可能的内存泄漏和CollectionChanged在添加多个项目时引发不必要的事件。它还提供了对来自代码隐藏的分组、排序、过滤、当前项目和事件的CurrentChanged轻松访问。CurrentChanging

在视图模型中:

public ViewableCollection<IHost> MyItems { get; set; }

初始化集合:

this.MyItems  = new ViewableCollection<IHost>();

// decide how your items will be sorted (important: first sort groups, then items in groups)
this.MyItems.View.SortDescriptions.Add(new SortDescription("HostType", ListSortDirection.Ascending)); // sorting of groups
this.MyItems.View.SortDescriptions.Add(new SortDescription("Hostname", ListSortDirection.Ascending)); // sorting of items

PropertyGroupDescription groupDescription = new PropertyGroupDescription("HostType");
this.MyItems.View.GroupDescriptions.Add(groupDescription);

this.MyItems.View.CurrentChanged += MyItems_CurrentChanged;

this.MyItems.AddRange(new IHost[] {
           new HistoryItem { Hostname = "ccc" },
           new HistoryItem { Hostname = "aaa" },
           new HistoryItem { Hostname = "xxx" },
           new FavoriteItem { Hostname = "vvv" },
           new FavoriteItem { Hostname = "bbb" },
           new FavoriteItem { Hostname = "ttt" } });

选择项目时将执行此代码:

private void MyItems_CurrentChanged(object sender, EventArgs e)
    {
        Console.WriteLine("Selected item: " + this.MyItems.CurrentItem?.Hostname);
    }

这是ComboBox与分组的xaml(使用ViewableCollection,您需要绑定ItemsSourceMyItems.View而不是直接绑定到MyItems):

<ComboBox ItemsSource="{Binding MyItems.View, Mode=OneWay}"
              IsSynchronizedWithCurrentItem="True"
              DisplayMemberPath="DisplayText">
        <ComboBox.GroupStyle>
            <GroupStyle>
                <GroupStyle.HeaderTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding Path=Items.CurrentItem.HostType, StringFormat=[{0}]}"/>
                    </DataTemplate>
                </GroupStyle.HeaderTemplate>
            </GroupStyle>
        </ComboBox.GroupStyle>
    </ComboBox>

结果:

组合框代码结果


[DoNotNotify]
public class ViewableCollection<T> : ObservableCollection<T>
{
    private ListCollectionView _View;

    public ViewableCollection(IEnumerable<T> items)
        : base(items) { }

    public ViewableCollection()
        : base() { }

    [XmlIgnore]
    public ListCollectionView View
    {
        get
        {
            if (_View == null)
            {
                _View = new ListCollectionView(this);
                _View.CurrentChanged += new EventHandler(InnerView_CurrentChanged);
            }
            return _View;
        }
    }

    [XmlIgnore]
    public T CurrentItem
    {
        get
        {
            return (T)this.View.CurrentItem;
        }
        set
        {
            this.View.MoveCurrentTo(value);
        }
    }

    private void InnerView_CurrentChanged(object sender, EventArgs e)
    {
        this.OnPropertyChanged(new PropertyChangedEventArgs("CurrentItem"));
    }

    public void AddRange(IEnumerable<T> range)
    {
        if (range == null)
            throw new ArgumentNullException("range");

        foreach (T item in range)
        {
            this.Items.Add(item);
        }

        this.OnPropertyChanged(new PropertyChangedEventArgs("Count"));
        this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
        this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }

    public void ReplaceItems(IEnumerable<T> range)
    {
        if (range == null)
            throw new ArgumentNullException("range");

        this.Items.Clear();
        foreach (T item in range)
        {
            this.Items.Add(item);
        }

        this.OnPropertyChanged(new PropertyChangedEventArgs("Count"));
        this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
        this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }

    public void RemoveItems(IEnumerable<T> range)
    {

        if (range == null)
            throw new ArgumentNullException("range");

        foreach (T item in range)
        {
            this.Items.Remove(item);
        }

        this.OnPropertyChanged(new PropertyChangedEventArgs("Count"));
        this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
        this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }

    public void ClearAll()
    {
        IList old = this.Items.ToList();
        base.Items.Clear();
        this.OnPropertyChanged(new PropertyChangedEventArgs("Count"));
        this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
        this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }

    public void CallCollectionChaged()
    {
        this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }

    // necessary for xml easy serialization using [XmlArray] attribute
    public static implicit operator List<T>(ViewableCollection<T> o)
    {
        return o == null ? default(List<T>) : o.ToList();
    }

    // necessary for xml easy serialization using [XmlArray] attribute
    public static implicit operator ViewableCollection<T>(List<T> o)
    {
        return o == default(List<T>) || o == null ? new ViewableCollection<T>() : new ViewableCollection<T>(o);
    }
}

上面的代码是一个工作示例。我正在使用 nuget 包PropertyChanged2.Fody来注入PropertyChanged通知。


推荐阅读