0

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

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

但是,如果我随后修改了任何书籍,或者添加了任何书籍,那么在以某种方式刷新绑定之前,列表项的文本颜色不会更新。

4

3 回答 3

0

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

于 2012-09-13T12:05:29.997 回答
0

我试图运行你的代码。

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

您的代码中缺少某些部分。

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

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

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

根据这个qa,当绑定在内部时MultiBinding-ObservableCollection在这种情况下 - 需要引发更改的事件。

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

于 2012-09-13T14:34:37.403 回答
0

基于这个StackOverflow 问题和这个链接的代码示例,我想出了一个我相当满意的解决方案。

我创建了一个额外的类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-13T14:56:59.557 回答