如何在ItemsControl组中绑定SubTotal

时间:2011-08-22 23:26:48

标签: c# wpf xaml data-binding

想象一下类似DTO的课程:

class LineItem : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    public string Description { get; set; }

    private decimal m_Amount;
    public decimal Amount
    {
        get { return m_Amount; }
        set
        {
            if (m_Amount == value)
                return;
            m_Amount = value;
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs("Amount"));
        }
    }
}

这样的绑定:

<ItemsControl ItemsSource="{Binding LineItems}">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <DockPanel>
                <TextBox Text="{Binding Amount}"
                        DockPanel.Dock="Right" Width="50" />
                <TextBlock Text="{Binding Description}" />
            </DockPanel>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

这看起来像是:

enter image description here

现在,我想在底部总计。此外,我希望它在金额变化时更新。

这样的事情:

<TextBlock HorizontalAlignment="Right"
            Text="{Binding LineItems, 
            Converter={StaticResource MyConverter}}" />

为此:

enter image description here

但是什么是MyConverter?而且,它甚至是一种正确的方法吗?

我的问题:

这不起作用,因为转换器仅在第一次绑定时被调用。我希望它反映用户的变化,我需要处理未知数量的LineItems。当然,我不是第一个打到这个的人。 有办法吗?

2 个答案:

答案 0 :(得分:2)

您可以绑定到专门代表AverageAmount(在“视图模型”上)的属性,并确保在PropertyChanged上为您发送更改通知的每个LineItem对于AverageAmount属性,允许模型计算值以及重新获取新值的UI。 Maleak's example shows exactly that

但是,仔细考虑这样做的开销,我会看一下像BindableLinqObtics(或Continuous Linq)那样应该处理所有依赖关系分析和更改的内容通知。我们已经使用了BindableLinq并取得了巨大的成功,但是在这个时候,它并没有被启动它的人主动维护。

修改:

在不使用上面提到的那些库(删除处理集合和属性更改事件的管道)的情况下给出一个后端示例:

public class ItemListViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    private readonly ObservableCollection<ItemViewModel> _items = new ObservableCollection<ItemViewModel>();

    public ItemListViewModel()
    {
        _items.CollectionChanged += OnItemsChanged;
    }

    public ICollection<ItemViewModel> Items { get { return _items; } }

    private void OnItemsChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        switch (e.Action)
        {
            case NotifyCollectionChangedAction.Add:
                e.NewItems.Cast<ItemViewModel>().ToList().ForEach(iv => iv.PropertyChanged += OnItemPropertyChanged);
                break;
            case NotifyCollectionChangedAction.Remove:
                e.OldItems.Cast<ItemViewModel>().ToList().ForEach(iv => iv.PropertyChanged -= OnItemPropertyChanged);
                break;
            default:
                throw new NotImplementedException();
        }
    }

    private void OnItemPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == "Value")
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs("AverageValue"));
        }
    }

    public double AverageValue
    {
        get { return Items.Average(iv => iv.Value); }
    }
}

public class ItemViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    public string Family { get; set; }
    private int m_Value;
    public int Value
    {
        get { return m_Value; }
        set
        {
            if (m_Value == value)
                return;

            m_Value = value;
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs("Value"));
        }
    }
}

然后,XAML中的ItemsControl直接绑定到视图模型的Items属性,平均值绑定到AverageValue属性。它现在将处理所需的通知。

要在另一个级别添加分组,您必须引入另一个类“ItemGroupViewModel”,该类将监视父级的Items集合以进行更改。我会将属性更改侦听器添加到所有项目,然后如果他们更改其Family属性,则从本地Items集合添加/删除。如果他们更改了Value属性,则为AverageValue属性触发PropertyChanged。

注意:BindableLinq也支持分组操作。

答案 1 :(得分:1)

结果是这样的:

enter image description here

这是CS:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    // used to force databinding to refresh
    public int FakeProperty
    {
        get { return (int)GetValue(FakePropertyProperty); }
        set { SetValue(FakePropertyProperty, value); }
    }
    public static readonly DependencyProperty FakePropertyProperty =
        DependencyProperty.Register("FakeProperty", 
        typeof(int), typeof(MainWindow), new UIPropertyMetadata(0));

    private void TextBox_TextChanged(object sender, 
        System.Windows.Controls.TextChangedEventArgs e)
    {
        FakeProperty++;
    }

}

public class Item : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    public string Family { get; set; }
    private int m_Value;
    public int Value
    {
        get { return m_Value; }
        set
        {
            if (m_Value == value)
                return;
            m_Value = value;
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs("Value"));
        }
    }
}

public class Items : ObservableCollection<Item>
{
    public Items()
    {
        this.Add(new Item { Family = "One", Value = 1 });
        this.Add(new Item { Family = "One", Value = 2 });
        this.Add(new Item { Family = "Two", Value = 3 });
        this.Add(new Item { Family = "Two", Value = 4 });
        this.Add(new Item { Family = "Two", Value = 5 });
        this.Add(new Item { Family = "Three", Value = 6 });
        this.Add(new Item { Family = "Three", Value = 7 });
    }
}

public class SumConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, 
                    object parameter, System.Globalization.CultureInfo culture)
    {
        var _Default = 0;
        if (values == null || values.Length != 2)
            return _Default;
        var _Collection = values[0] as System.Collections.IEnumerable;
        if (_Collection == null)
            return _Default;
        var _Items = _Collection.Cast<Item>();
        if (_Items == null)
            return _Default;
        var _Sum = _Items.Sum(x => x.Value);
        return _Sum;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, obje
                    ct parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

这个XAML:

xmlns:sort="clr-namespace:System.ComponentModel;assembly=WindowsBase"
xmlns:sys="clr-namespace:System;assembly=mscorlib" Name="This"

<Window.Resources>
    <local:Items x:Key="MyData" />
    <local:SumConverter x:Key="MyConverter" />
    <CollectionViewSource x:Key="MyView" Source="{StaticResource MyData}">
        <CollectionViewSource.GroupDescriptions>
            <PropertyGroupDescription PropertyName="Family" />
        </CollectionViewSource.GroupDescriptions>
        <CollectionViewSource.SortDescriptions>
            <sort:SortDescription PropertyName="Value" Direction="Ascending" />
        </CollectionViewSource.SortDescriptions>
    </CollectionViewSource>
</Window.Resources>

<StackPanel>

    <ItemsControl ItemsSource="{Binding Source={StaticResource MyView}}"
        Name="MyItemsControl">
        <ItemsControl.Resources>
            <Style TargetType="{x:Type GroupItem}">
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="{x:Type GroupItem}">
                            <StackPanel>
                                <!-- group header -->
                                <Border Padding="10,5,0,5" Margin="0,10,0,10" 
                                        Background="Gainsboro" CornerRadius="10">
                                    <TextBlock FontWeight="Bold" 
                                            Text="{Binding Name}" />
                                </Border>
                                <!-- group items -->
                                <ItemsPresenter Margin="10,0,0,0"/>
                                <!-- group footer -->
                                <Border BorderBrush="Black" 
                                            BorderThickness="0,.5,0,0"
                                            Margin="0,5,0,10">
                                    <TextBlock Width="100" 
                                            HorizontalAlignment="Right" 
                                            TextAlignment="Right" 
                                            Padding="0,0,5,0">
                                        <TextBlock.Text>
                                            <MultiBinding 
                                             StringFormat="{}{0:C}"
                                             Converter="{StaticResource MyConverter}">
                                                <Binding Path="Items" />
                                                <Binding Path="FakeProperty" 
                                                        ElementName="This"/>
                                            </MultiBinding>
                                        </TextBlock.Text>
                                    </TextBlock>
                                </Border>
                            </StackPanel>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>
        </ItemsControl.Resources>
        <ItemsControl.GroupStyle>
            <GroupStyle ContainerStyle="{x:Null}" />
        </ItemsControl.GroupStyle>
        <ItemsControl.ItemTemplate>
            <DataTemplate DataType="ItemsPresenter">
                <DockPanel>
                    <TextBox Text="{Binding Value, StringFormat={}{0:C}, 
                        UpdateSourceTrigger=PropertyChanged}" 
                        TextChanged="TextBox_TextChanged" 
                        TextAlignment="Right" DockPanel.Dock="Right"
                        Width="100" />
                    <TextBlock Text="{Binding Value, 
                        StringFormat={}Value is {0}}" 
                        FontWeight="Bold" Foreground="DimGray" />
                </DockPanel>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>

    <!-- list footer -->
    <Border BorderBrush="Black" BorderThickness="0,.5,0,0" Margin="0,5,0,10">
        <TextBlock Width="100" HorizontalAlignment="Right" TextAlignment="Right" 
                   Padding="0,0,5,0" FontWeight="Bold">
            <TextBlock.Text>
                <MultiBinding Converter="{StaticResource MyConverter}" 
                              StringFormat="{}{0:C}">
                    <Binding Path="ItemsSource" ElementName="MyItemsControl" />
                    <Binding Path="FakeProperty" ElementName="This"/>
                </MultiBinding>
            </TextBlock.Text>
        </TextBlock>
    </Border>

</StackPanel>

弄清楚多么噩梦!