数据绑定基于符合某些条件的集合中的项目

时间:2012-09-13 11:54:03

标签: c# wpf data-binding collections

假设我有一个带有列表框和文本框的基本控件,其中列表框绑定到一组对象并具有基本数据模板

<DockPanel LastChildFill="True">
    <TextBlock DockPanel.Dock="Top">Book name</TextBlock>
    <TextBox x:Name="bookNameTextBox" DockPanel.Dock="Top" />
    <TextBlock DockPanel.Dock="Top">Authors</TextBlock>
    <ListBox ItemsSource="{Binding Authors}">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <TextBlock Text="{Binding Name}" />
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</DockPanel>

public class Author : INotifyPropertyChanged
{
    public string Name { get; set; }
    public ObservableCollection<Book> Books { get; }
}

public class Book : INotifyPropertyChanged
{
    public string Name { get; }
}

我想要做的是让列表框中项目的颜色发生变化,具体取决于作者是否有任何与所提供名称相匹配的书籍,例如

Colour = author.Books.Any(b => b.Name.StartsWith(bookNameTextBox.Text)) ? Red : Black;

我最初认为我可以使用MultiBinding和转换器来做到这一点,但是我无法弄清楚如何在将书籍集添加到书籍集合中或从书籍集合中移除项目时更新绑定,或者他的名字是什么书改了。

如何以这样的方式执行此操作,以便颜色能够正确更新以响应可能影响我逻辑的所有各种更改?,例如

  • 任何更改图书的名称
  • 正在从集合中添加和删除的图书
  • bookNameTextBox文本框中的文字正在更改

我的MultiBinding看起来像这样

<TextBlock.Style>
    <Style TargetType="TextBlock">
        <Style.Triggers>
            <DataTrigger Value="True">
                <DataTrigger.Binding>
                    <MultiBinding Converter="{StaticResource MyConverter}">
                        <Binding Path="Books" />
                        <Binding Path="Text" ElementName="bookNameTextBox" />
                    </MultiBinding>
                </DataTrigger.Binding>
                <Setter Property="Foreground" Value="Red" />
            </DataTrigger>
        </Style.Triggers>
    </Style>
</TextBlock.Style>

我的转换器(实现IMultiValueConverter)看起来像这样

public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
    var text = (string)values.First(v => v is string);
    var books = (IEnumerable<Book>)values.First(v => v is IEnumerable<Book>);
    return books.Any(b => b.Name.StartsWith(text));
}

然而,如果我随后修改了任何书籍,或者添加了任何书籍,那么列表项目的文本颜色将不会更新,直到绑定以某种方式刷新。

3 个答案:

答案 0 :(得分:0)

我认为您要搜索的内容是Binding Converter每个绑定的项目都会收到一个电话,您可以在其中放置您的决策逻辑并返回相应的结果(颜色在您的情况)。

答案 1 :(得分:0)

我试图运行你的代码。

更改bookNameTextbox.Text正在调用转换器,结果是正确的。

您的代码中缺少部分内容。

您不会调用PropertyChanged事件。您必须这样做,因为视图不会收到有关更改的通知。因此,在设置属性后,请调用此事件。

足够使用简单的绑定。我的意思是ObservableCollection在其中的项目正确发送道具更改通知时会不断更新视图。

在这种情况下,使用MultiBinding时我们还需要一些东西 - 我认为这很奇怪。

根据此q-a,如果绑定位于MultiBindingObservableCollection - 在这种情况下 - 需要提升已更改的事件。

因此,在修改Books集合后,我调用了Books属性的PropertyChanged事件,结果与预期一致。

答案 2 :(得分:0)

基于this StackOverflow问题和this链接代码示例,我提出了一个我非常满意的解决方案。

我创建了一个继承自AuthorInfo的其他课程FrameworkElement,并将此课程的实例与我的TextBlock放在一起,就像这样

<DataTemplate>
    <StackPanel>
        <Local:AuthorInfo Collection="{Binding Books}" Text="{Binding Text, ElementName=_bookNameTextBox}" x:Name="_authorInfo" />
        <TextBlock Text="{Binding Name}">
            <TextBlock.Style>
                <Style TargetType="TextBlock">
                    <Style.Triggers>
                        <DataTrigger Value="True" Binding="{Binding BookMatches, ElementName=_authorInfo}">
                            <Setter Property="Foreground" Value="Red" />
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </TextBlock.Style>
        </TextBlock>
    </StackPanel>
</DataTemplate>

此类具有要监视的集合和要查找的文本值的依赖项属性,并公开BookMatches属性,该属性指示该书是否与提供的字符串匹配。这是我的触发器绑定的属性。

为了确保在修改列表中的列表或项目时更新属性值,此类会跟踪订阅和取消订阅各种属性更改事件 - 它看起来有点像这样

public class AuthorInfo : FrameworkElement
{
    public static readonly DependencyProperty TextProperty =
        DependencyProperty.Register("Text", typeof(string), typeof(AuthorInfo), new PropertyMetadata(default(string), PropertyChangedCallback));

    public static readonly DependencyProperty CollectionProperty =
        DependencyProperty.Register("Collection", typeof (IEnumerable), typeof (AuthorInfo), new PropertyMetadata(default(IEnumerable), PropertyChangedCallback));

    private static readonly DependencyPropertyKey ValuePropertyKey =
        DependencyProperty.RegisterReadOnly("Value", typeof (bool), typeof (AuthorInfo), new PropertyMetadata(default(bool)));

    public static readonly DependencyProperty ValueProperty = ValuePropertyKey.DependencyProperty;

    public bool BookMatches
    {
        get
        {
            return (bool) GetValue(ValueProperty);
        }
        set
        {
            SetValue(ValuePropertyKey, value);
        }
    }

    public IEnumerable Collection
    {
        get
        {
            return (IEnumerable)GetValue(CollectionProperty);
        }
        set
        {
            SetValue(CollectionProperty, value);
        }
    }

    public string Text
    {
        get
        {
            return (string)GetValue(TextProperty);
        }
        set
        {
            SetValue(TextProperty, value);
        }
    }

    protected void UpdateValue()
    {
        var books = Collection == null ? Enumerable.Empty<Book>() : Collection.Cast<Book>();
        BookMatches = !string.IsNullOrEmpty(Text) && books.Any(b => b.Name.StartsWith(Text));
    }

    private void CollectionSubscribe(INotifyCollectionChanged collection)
    {
        if (collection != null)
        {
            collection.CollectionChanged += CollectionOnCollectionChanged;
            foreach (var item in (IEnumerable)collection)
            {
                CollectionItemSubscribe(item as INotifyPropertyChanged);
            }
        }
    }

    private void CollectionUnsubscribe(INotifyCollectionChanged collection)
    {
        if (collection != null)
        {
            collection.CollectionChanged -= CollectionOnCollectionChanged;
            foreach (var item in (IEnumerable)collection)
            {
                CollectionItemUnsubscribe(item as INotifyPropertyChanged);
            }
        }
    }

    private void CollectionItemSubscribe(INotifyPropertyChanged item)
    {
        if (item != null)
        {
            item.PropertyChanged += ItemOnPropertyChanged;
        }
    }

    private void CollectionItemUnsubscribe(INotifyPropertyChanged item)
    {
        if (item != null)
        {
            item.PropertyChanged -= ItemOnPropertyChanged;
        }
    }

    private void CollectionOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
    {
        if (args.OldItems != null)
        {
            foreach (var item in args.OldItems)
            {
                CollectionItemUnsubscribe(item as INotifyPropertyChanged);
            }
        }
        if (args.NewItems != null)
        {
            foreach (var item in args.NewItems)
            {
                CollectionItemSubscribe(item as INotifyPropertyChanged);
            }
        }
        UpdateValue();
    }

    private void ItemOnPropertyChanged(object sender, PropertyChangedEventArgs args)
    {
        UpdateValue();
    }

    private static void PropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args)
    {
        var aggregator = (AuthorInfo)dependencyObject;
        if (args.Property == CollectionProperty)
        {
            aggregator.CollectionUnsubscribe(args.OldValue as INotifyCollectionChanged);
            aggregator.CollectionSubscribe(args.NewValue as INotifyCollectionChanged);
        }
        aggregator.UpdateValue();
    }
}

这并不是说这个订阅/取消订阅业务很难,只是它有点繁琐 - 这种方式变化通知的东西与表示逻辑分离。它也应该很容易重构,以便在基类中包含所有更改通知,以便可以将此逻辑重用于其他类型的聚合。