【问题标题】:Databind based on items in a collection matching some criteria基于符合某些条件的集合中的项目进行数据绑定
【发布时间】:2012-09-13 11:54:02
【问题描述】:

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

<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));
}

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

【问题讨论】:

  • 你是如何尝试使用 MultiBinding 的?
  • @MiklósBalogh 我已经更新了问题
  • 我认为您将需要迭代作者并在名称上调用 NotifyPropertyChanged。

标签: c# wpf data-binding collections


【解决方案1】:

我认为您正在搜索的是Binding Converter每个绑定的项目都会收到一个调用,您可以在其中放置您的决策逻辑并返回适当的结果(在您的情况下为颜色) .

【讨论】:

  • 我试过这个,但是当项目被添加到集合中或从集合中删除或修改集合中的单个项目时,它没有更新绑定。
  • @Justin:这取决于 如何 你的绑定是如何定义的。如果需要,您还可以强制执行 binidn 评估,以便再次跟踪 cpnverter。
【解决方案2】:

我试图运行你的代码。

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

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

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

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

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

根据这个q-a,当绑定在MultiBinding 内时,ObservableCollection - 在这种情况下 - 需要引发 changed 事件。

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

【讨论】:

  • 我想避免在对 Books 集合进行更改时明确告诉 UI 刷新 - 毕竟这就是更改通知的用途 :)
  • @Justin 没错,你需要通知。名称未更改,因此不会通知。
【解决方案3】:

基于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();
    }
}

并不是这个订阅/取消订阅业务难,只是它有点繁琐——这样繁琐的更改通知内容就与呈现逻辑分离了。重构它以在基类中包含所有更改通知也应该很容易,以便可以将此逻辑重用于其他类型的聚合。

【讨论】:

    猜你喜欢
    • 2012-09-22
    • 1970-01-01
    • 2019-10-05
    • 2012-01-27
    • 2021-11-09
    • 2011-05-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多