数据绑定基于符合某些条件的集合中的项目 [英] Databind based on items in a collection matching some criteria

查看:111
本文介绍了数据绑定基于符合某些条件的集合中的项目的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

 < DockPanel LastChildFill =True> 
< TextBlock DockPanel.Dock =Top>书名< / TextBlock>
< TextBox x:Name =bookNameTextBoxDockPanel.Dock =Top/>
< TextBlock DockPanel.Dock =Top>作者< / TextBlock>
< ListBox ItemsSource ={Binding Authors}>
< ListBox.ItemTemplate>
< DataTemplate>
< TextBlock Text ={Binding Name}/>
< / DataTemplate>
< /ListBox.ItemTemplate>
< / ListBox>
< / DockPanel>

public class作者:INotifyPropertyChanged
{
public string Name {get;组; }
public ObservableCollection< Book>书籍{get;
}

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

我想做的是在列表框中显示项目的颜色根据作者是否具有与提供的名称相匹配的任何书籍而更改,例如

  Color = author.Books.Any(b => b.Name.StartsWith(bookNameTextBox.Text))?红黑; 

我最初以为可以使用MultiBinding和转换器,但是我无法工作当您将书籍添加到书籍集合中或从书籍集合中删除时,如何更改绑定,或者更改了书籍名称。



我如何这样做的方式是,颜色会正确更新以响应可能影响我的逻辑的所有各种变化?例如




  • 任何图书的名称更改

  • 从集合中添加和删除的图书

  • bookNameTextBox 文本框更改






我的MultiBinding看起来像这个

 < TextBlock.Style> 
< Style TargetType =TextBlock>
< Style.Triggers>
< DataTrigger Value =True>
< DataTrigger.Binding>
< MultiBinding Converter ={StaticResource MyConverter}>
< Binding Path =Books/>
< Binding Path =TextElementName =bookNameTextBox/>
< / MultiBinding>
< /DataTrigger.Binding>
< Setter Property =ForegroundValue =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是string);
var books =(IEnumerable< Book>)values.First(v => v是IEnumerable< Book>);
return books.Any(b => b.Name.StartsWith(text));
}

但是,如果我修改了任何图书,或添加了任何图书,列表项的文本颜色将不会更新,直到绑定被刷新为止。

解决方案

我已经提出了一个解决方案我很乐意基于这个 StackOverflow问题和链接代码示例。



我创建了一个从 FrameworkElement 继承的附加类 AuthorInfo ,并将这个类的一个实例放在我的 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 =TrueBinding ={Binding BookMatches,ElementName = _authorInfo}>
< Setter Property =ForegroundValue =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),新的PropertyMetadata(默认(IEnumerable),PropertyChangedCallback));

private static readonly DependencyPropertyKey ValuePropertyKey =
DependencyProperty.RegisterReadOnly(Value,typeof(bool),typeof(AuthorInfo),新的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(argsOldItems中的var item)
{
CollectionItemUnsubscribe(item as INotifyPropertyChanged);
}
}
if(args.NewItems!= null)
{
foreach(argsNewItems中的var item)
{
CollectionItemSubscribe(项目为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();
}
}

这不是这个订阅/取消订阅业务是困难的,它只是它有点fiddly - 这种方式的改变通知的东西是从演示逻辑分离。它也应该足够重构,以便在基类中具有所有更改通知,以便该逻辑可以重新用于其他类型的聚合。


Suppose I have a basic control with a listbox and a text box, where the listbox is bound to a collection of objects and has a basic data template

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

What I want to do is have the colours of the items in the listbox change depending on if that author has any books that match the supplied name, for example

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

I initially thought that I could do this using a MultiBinding and a converter, however I couldn't work out how to get the binding to update when items were added to / removed from the books collection, or whent he name of a book changed.

How can I do this in such a way that the colour will update correctly in response to all of the various changes that could affect my logic? e.g.

  • The name of any of the books changing
  • Books being added and removed from the collection
  • The text in the bookNameTextBox text box changing

My MultiBinding looked like this

<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>

And my converter (which implemented IMultiValueConverter) looked like this

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

This worked however if I then modified any of the books, or added any books the text colour of the list item would not update until the binding was somehow refreshed.

解决方案

I've come up with a solution that I'm reasonably happy with loosely based on this StackOverflow question and this linked code sample.

I created an additional class AuthorInfo that inherits from FrameworkElement and put an instance of this class alongside my TextBlock, like so

<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>

This class has dependency properties for both the collection to monitor and the text value to look for and exposes a BookMatches property that indicates whether or not the book matches the supplied string. This is the property that my trigger binds to.

In order to make sure that the property value is updated when the list or items in the list are modified this class keeps track of subscribing to and unsubscribing from the various property changed events - it looks a bit like this

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

Its not that this subscription / unsubscription business is difficult, its just that its a bit fiddly - this way the fiddly change notification stuff is separated from the presentation logic. It should also be easy enough to refactor this out to have all of the change notification in a base class, so that this logic can be re-used for other types of aggregation.

这篇关于数据绑定基于符合某些条件的集合中的项目的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆