ObservableCollection 到 3 个列表视图之一取决于一些 属性

ObservableCollection to 1 of 3 listviews depending on some property

我目前有一个名为 MyList 的 ObservableCollection,它绑定到列表视图的项目源。它还在 MyList 中添加和删除项目。

我想要的是根据某些条件在每个列表视图中添加项目。更具体地说,如果 属性 Status 是 "Yes" 项目应该转到第一个列表视图 MyListview,如果是 "No" 则转到第二个列表视图 MySecondListviewIf Status=="" 和 属性 Date 这是一个 DateTime 属性 点今天,然后它转到第三个列表视图。

我的主页代码是:

public sealed partial class MainPage : Page
    {
        private ObservableCollection<MyClass> MyList = new ObservableCollection<MyClass>();
        public MainPage()
        {
            this.InitializeComponent();
            DataContext = MyList;
        }
         private void Add_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
        {
            MyList.Add(new MyClass("Yes", new DateTime(2015, 5, 4)));
        }

        private void Delete_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
        {
            MyList.Remove((MyClass)MyListview.SelectedItem);
        }
    }

我的 XAML 主页是:

<Page
    x:Class="App17.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App17"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Viewbox>

        <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" Height="768" Width="1366">
            <Button x:Name="Delete" Content="Delete" HorizontalAlignment="Left" Height="116" Margin="376,89,0,0" VerticalAlignment="Top" Width="224" Click="Delete_Click"/>
            <Button x:Name="Add" Content="Add" HorizontalAlignment="Left" Height="116" Margin="111,89,0,0" VerticalAlignment="Top" Width="214" Click="Add_Click"/>
            <ListView x:Name="MyListview" HorizontalAlignment="Left" Height="497" Margin="71,261,0,0" VerticalAlignment="Top" Width="349" ItemsSource="{Binding}"/>
            <ListView x:Name="MySecondListview" HorizontalAlignment="Left" Height="497" Margin="468,261,0,0" VerticalAlignment="Top" Width="317"/>
            <ListView x:Name="MyThirdListview" HorizontalAlignment="Left" Height="497" Margin="893,261,0,0" VerticalAlignment="Top" Width="317"/>

        </Grid>
    </Viewbox>
</Page>

我的Class代码是:

class MyClass 
{ 
    public string Status { get; set; } 
    public DateTime Date { get; set; }

    public MyClass(string status, DateTime date) 
    {
        Status = status;
        Date = date; 
    } 
}

从 MVVM 的角度来看,您将拥有 3 个 ObservableCollections,其中每个代表每个列表视图。但是 "model" 会有一个统一的列表。正如评论中所建议的那样,要使用一个 ObservableCollection 正确地执行此操作将需要创建一个视图(至少要使用绑定。) 我会说最明显的解决方案通常是最不容易阅读的。

public ObservableCollection<MyClass> MyFirstListCollection...
public ObservableCollection<MyClass> MySecondListCollection...

通常最好尽最大努力将视图逻辑与视图模型分开。这将使将问题隔离到一个或另一个变得容易得多。将它们与直接操作屏幕上的列表视图结合得越多,就越难看出发生了什么。 然后在 XAML 中你可以绑定到它们

<ListBox ItemSource="{Binding MyFirstListCollection}"... (rest of the margin stuff)

添加事件处理程序现在可以只添加到所需的可观察集合,从而允许绑定更新视图。

请注意,您还可以将任意对象分配为 DataContext,而在生产中这不是您可以执行的最佳做法:

this.DataContext = this;

然后 ObservableCollections 必须是 "properties" 而不仅仅是数据成员但是添加 getter 和 setter 是微不足道的 例如,如果你从一个看起来像的视图模型开始(为了简单起见,我省略了很多代码,比如没有在 ctor 中创建 Observable 集合):

class ViewModel
{
    MyModel model;
    ObservableCollection<MyClass> MyFirstList { get; set; }
    ObservableCollection<MyClass> MySecondList  {   get; set;   }
    ObservableCollection<MyClass> MyThirdList { get; set; }     
    public ViewModel(MyModel model) {
         this.model = model;
         RefreshCollections();
    }

    public void RefreshCollections() {
         //Clear the observable collections.
         MyFirstList.Clear();
         foreach(var item in model.MyUnifiedList) {
              //TODO: add to the correct collection based on criteria.
         }
    }
}

然后我会将您的数据上下文附加到此 class。当然提供了一个工作模型。 "new MyModel()" 可以改为读取文件或其他文件。

this.DataContext = new ViewModel(new MyModel())

最后你的 class 看起来像。

class MyModel {
     List<MyClass> MyUnifiedList;
}

您可能希望使用某种事件来引起 UI 刷新。但从某种意义上说,我认为你的单一列表标准使问题复杂化。我个人会一直列出 3 个列表。仅仅因为它们是同一个对象并不意味着它们需要在同一个列表中。无需过度优化,相信我,您不会后悔的。 当然,当您需要更改某些内容时,您会刷新所有内容。 通常,您希望考虑 MVVM,以便您的视图在视图模型中呈现信息,而模型实现逻辑。实际做的工作。 (在合理的情况下)这不是一个硬性的快速规则,但对 MVVM 的一些阅读将有助于区分这些地方。有很多方法可以让模型刷新视图模型。

呃。所以,我没有意识到这一点,但事实证明 Winrt(即 Windows 商店应用程序)不支持 ICollectionView 中的过滤。似乎每次转身,我都会发现另一个 XAML/WPF 功能被 Winrt 莫名其妙地遗漏了,这就是其中之一。

因此,虽然这在 WPF 中很容易完成(只需创建一个 CollectionViewSource object,订阅 Filter 事件,然后绑定到 object的View),结果证明这在Winrt中有点麻烦。

已经有一些相关文章,包括Stack Overflow上的一篇提供替代方案的文章。例如:

第二个是支持过滤和其他操作的非常精细的实现。它对您的目的来说有点矫枉过正,但您将来可能会发现您想使用它包含的功能。

第一个示例虽然在规模上更适合您的问题,但处理问题的方式与我略有不同。特别是,它实现了一个 ObservableCollection<T> subclass ,而该子 class 又具有一个视图。这会阻止它对 "one collection, many views" 场景有用。

所以我写了一个ObservableCollection<T>,它是一脉相承的,但它本身就是视图,并引用了ObservableCollection<T>的一个独立实例。您可以根据需要使用此 class(我称之为 FilteredObservableCollection<T>)的任意多个实例,同时仍保持单一来源 collection。当源 collection 发生变化时,每个视图都会根据过滤器根据需要自行更新。


在这种方法中(我的 class 以及我自己的最初灵感),绑定视图本身就是一个 ObservableCollection<T>。特别是,这是 而不是 和 read-only collection。非常重要的是,这些 "view" object 的任何使用者都不要尝试直接修改视图 object,即使他们可以。他们应该只显示 object.

上面的第二个 link 在这方面更好,因为它实现了一个实际的 ICollectionView 接口,解决了可变性问题。坦率地说,从代码维护的角度来看,这样做会更好,可以更清楚地说明什么 object 是真正的列表,什么 object 只是视图。

但事实是,这种方式很多更简单,更容易实现。小心使用它,您不会受伤。 :)


另一个需要理解的非常重要的限制是,此实现将 不会 更新已查看的 collection 过滤器逻辑的更改(即,如果您传递 Predicate<T>本身取决于某处的某些可变状态)或显示数据的更改(即,如果过滤器检查的 属性 在一个或多个显示的数据项中被修改)。

可以通过多种方式解决这些限制,但这样做会显着增加此答案的复杂性。我想尽量保持简单。我希望仅仅指出这些限制就足够了,以确保如果您 运行 遇到需要更具反应性的视图实现的情况,您就会意识到这些限制并且知道您必须扩展此解决方案可满足您的需求。

class 看起来像这样:

public class FilteredObservableCollection<T> : ObservableCollection<T>
{
    private Predicate<T> _filter;

    public FilteredObservableCollection(ObservableCollection<T> source, Predicate<T> filter)
        : base(source.Where(item => filter(item)))
    {
        source.CollectionChanged += source_CollectionChanged;
        _filter = filter;
    }

    private void _Fill(ObservableCollection<T> source)
    {
        Clear();
        foreach (T item in source)
        {
            if (_filter(item))
            {
                Add(item);
            }
        }
    }

    private int this[T item]
    {
        get
        {
            int foundIndex = -1;
            for (int index = 0; index < Count; index++)
            {
                if (this[index].Equals(item))
                {
                    foundIndex = index;
                    break;
                }
            }
            return foundIndex;
        }
    }

    private void source_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        ObservableCollection<T> source = (ObservableCollection<T>)sender;

        switch (e.Action)
        {
        case NotifyCollectionChangedAction.Add:
            foreach (T item in e.NewItems)
            {
                if (_filter(item))
                {
                    Add(item);
                }
            }
            break;

        case NotifyCollectionChangedAction.Move:
            // Without a lot more work maintaining the view state, it would be just as hard
            // to figure out where the moved item should go, as it would be to just regenerate
            // the whole view. So just do the latter.
            _Fill(source);
            break;

        case NotifyCollectionChangedAction.Remove:
            foreach (T item in e.OldItems)
            {
                // Don't bother looking for the item if it was filtered out
                if (_filter(item))
                {
                    Remove(item);
                }
            }
            break;

        case NotifyCollectionChangedAction.Replace:
            for (int index = 0; index < e.OldItems.Count; index++)
            {
                T item = (T)e.OldItems[index];
                if (_filter(item))
                {
                    int foundIndex = this[item];

                    if (foundIndex == -1)
                    {
                        // i.e. should never happen
                        throw new Exception("internal state failure. object not present, even though it should be.");
                    }

                    T newItem = (T)e.NewItems[index];

                    if (_filter(newItem))
                    {
                        this[foundIndex] = newItem;
                    }
                    else
                    {
                        RemoveAt(foundIndex);
                    }
                }
                else
                {
                    // The item being replaced wasn't in the filtered
                    // set of data. Rather than do the work to figure out
                    // where the new item should go, just repopulate the
                    // whole list. (Same reasoning as for Move event).
                    _Fill(source);
                }
            }
            break;

        case NotifyCollectionChangedAction.Reset:
            _Fill(source);
            break;
        }
    }
}

因此,实施了 object 助手后,设置您的 UI 就很简单了。只需为您想要的每个数据视图创建上述 class 的新实例,然后绑定到该实例。

对于这个例子,我对你原来的 UI 进行了相当广泛的修改,以清理一些东西并更容易观察发生了什么。我添加了按钮来创建应该出现在每个视图中的项目,现在有四个视图:整个列表(您可以在其中 select 和删除项目),然后三个过滤选项各一个。

使用这种方法,DataContext 最终成为 this 而不是一个列表,因为我们想要访问各个视图。成员也需要更改为 public 属性,以便 XAML 绑定引擎可以找到它们。这个例子假设它们将被初始化一次并且永远不会改变;如果您希望绑定可更新,您需要创建这些依赖属性或在 MainPage class.

中实现 INotifyPropertyChanged

我还为你的 class 添加了一个 DataTemplate,这样每个列表中的每个项目的介绍都是有用的(即类型名称以外的内容)。

综上所述,XAML 最终看起来像这样:

<Page
    x:Class="TestSO30038588ICollectionView.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:TestSO30038588ICollectionView"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

  <Page.Resources>
    <DataTemplate x:Key="myClassTemplate">
      <StackPanel Orientation="Horizontal">
        <TextBlock Text="{Binding Status}"/>
        <TextBlock Text="{Binding Date}" Margin="20, 0, 0, 0"/>
      </StackPanel>
    </DataTemplate>
  </Page.Resources>
  <Viewbox>
    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" Height="768" Width="1366">
      <Grid.ColumnDefinitions>
        <ColumnDefinition/>
        <ColumnDefinition/>
        <ColumnDefinition/>
        <ColumnDefinition/>
      </Grid.ColumnDefinitions>
      <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>
        <RowDefinition/>
      </Grid.RowDefinitions>
      <Button x:Name="Delete" Content="Delete"
              HorizontalAlignment="Left" VerticalAlignment="Top"
              Height="116" Width="224"
              Grid.Row="0" Grid.Column="0"
              Click="Delete_Click"/>
      <Button x:Name="Toggle" Content="Toggle Yes/No"
              HorizontalAlignment="Left" VerticalAlignment="Top"
              Height="116" Width="224"
              Grid.Row="1" Grid.Column="0"
              Click="Toggle_Click"/>
      <Button x:Name="AddYes" Content="Add Yes"
              HorizontalAlignment="Left" VerticalAlignment="Top"
              Height="116" Width="214"
              Grid.Row="0" Grid.Column="1"
              Click="AddYes_Click"/>
      <Button x:Name="AddNo" Content="Add No"
              HorizontalAlignment="Left" VerticalAlignment="Top"
              Height="116" Width="214"
              Grid.Row="0" Grid.Column="2"
              Click="AddNo_Click"/>
      <Button x:Name="AddEmpty" Content="Add Empty"
              HorizontalAlignment="Left" VerticalAlignment="Top"
              Height="116" Width="214"
              Grid.Row="0" Grid.Column="3"
              Click="AddEmpty_Click"/>
      <ListView x:Name="AllElementsList"
                ItemTemplate="{StaticResource myClassTemplate}"
                HorizontalAlignment="Left" VerticalAlignment="Top"
                Grid.Row="2" Grid.Column="0"
                ItemsSource="{Binding MyList}"/>
      <ListView x:Name="MyListview"
                ItemTemplate="{StaticResource myClassTemplate}"
                HorizontalAlignment="Left" VerticalAlignment="Top"
                Grid.Row="1" Grid.Column="1" Grid.RowSpan="2"
                ItemsSource="{Binding yesList}"/>
      <ListView x:Name="MySecondListview"
                ItemTemplate="{StaticResource myClassTemplate}"
                HorizontalAlignment="Left" VerticalAlignment="Top"
                Grid.Row="1" Grid.Column="2" Grid.RowSpan="2"
                ItemsSource="{Binding noList}"/>
      <ListView x:Name="MyThirdListview"
                ItemTemplate="{StaticResource myClassTemplate}"
                HorizontalAlignment="Left" VerticalAlignment="Top"
                Grid.Row="1" Grid.Column="3" Grid.RowSpan="2"
                ItemsSource="{Binding emptyList}"/>
    </Grid>
  </Viewbox>
</Page>

MainPage 代码如下所示:

public sealed partial class MainPage : Page
{
    public ObservableCollection<MyClass> MyList { get; set; }
    public FilteredObservableCollection<MyClass> yesList { get; set; }
    public FilteredObservableCollection<MyClass> noList { get; set; }
    public FilteredObservableCollection<MyClass> emptyList { get; set; }

    public MainPage()
    {
        this.InitializeComponent();

        MyList = new ObservableCollection<MyClass>();
        yesList = new FilteredObservableCollection<MyClass>(MyList, item => item.Status == "Yes");
        noList = new FilteredObservableCollection<MyClass>(MyList, item => item.Status == "No");
        emptyList = new FilteredObservableCollection<MyClass>(MyList, item => item.Status == "" && item.Date.Date == DateTime.Now.Date);

        DataContext = this;
    }

    private void AddYes_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
    {
        MyList.Add(new MyClass("Yes", DateTime.Now));
    }

    private void AddNo_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
    {
        MyList.Add(new MyClass("No", DateTime.Now));
    }

    private void AddEmpty_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
    {
        MyList.Add(new MyClass("", DateTime.Now));
    }

    private void Delete_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
    {
        MyList.Remove((MyClass)AllElementsList.SelectedItem);
    }

    private void Toggle_Click(object sender, RoutedEventArgs e)
    {
        MyClass oldItem = (MyClass)AllElementsList.SelectedItem,
            newItem = new MyClass(oldItem.Status == "Yes" ? "No" : (oldItem.Status == "No" ? "Yes" : ""), oldItem.Date);

        MyList[AllElementsList.SelectedIndex] = newItem;
    }
}

通过以上内容,只能直接对 MyList object 进行更改。此列表的状态显示在页面上的第一个 ListView object 中(即 left-most 那个)。三个不同的按钮添加我配置为以不同方式过滤的 ems。这些项目被添加到主列表中,但随后三个过滤视图会自动更新自己以反映这些更改,您将在页面上的其他 ListView object 中看到(每个都在下面添加显示在 ListView) 中的项目的按钮。

希望对您有所帮助!


编辑:

评论中的一些附加点,我认为可以提高此答案的实用性:

  • 从每个 ListView 中删除项目(即给定 ListView 中的 SelectedItem)可以很容易地完成,只要您仍然只是从实际的 MyList collection;每个列表中的 object 引用都是您的原始 object,因此即使它们可能在某个特定视图中被 select 编辑,您始终可以从 MyList
  • 您可以修改单个 MyClass object,但是,如果不在此示例中进行额外的工作,过滤将不会改变。 IE。如果 MyClass 正确实现绑定功能(依赖属性或 INotifyPropertyChanged),将显示对项目属性的更改 on-screen,但不会影响过滤。上面的示例需要将 object 替换为 MyList 中具有新值的不同实例,以便重新过滤。