有没有办法在 WPF 中求和或平均属性?

Posted

技术标签:

【中文标题】有没有办法在 WPF 中求和或平均属性?【英文标题】:Is there a way to sum or average properties in WPF? 【发布时间】:2021-10-17 05:53:37 【问题描述】:

我有一个数据网格控件,我用它来显示很多值,例如价格、金额、每日变化百分比等。因此每个单元格都显示适当的值。

这些都是基础数据的绑定属性。

但有些列标题我想显示所有这些属性的总和或平均值。

例如对于价格、平均(价格)等。

现在我必须创建一个单独的类来为整个集合执行此操作并使用它们绑定到列标题,但它确实创建了更多代码和额外的层来维护。

这是否可以更优雅地完成,最好是在 XAML 中?所以我不必为聚合数据跟踪和引发更改的事件。

这是一列的代码:

<DataGridTemplateColumn Width="50" SortMemberPath="PriceChangeMonthly.InPercent" local:AttachedClass.ColName="PriceChangeMonthly">
    <DataGridTemplateColumn.Header>
        <TextBlock Text="Binding Path=(local:MarketData.PriceChangeMonthlyDisplay)"/>
    </DataGridTemplateColumn.Header>
    <DataGridTemplateColumn.CellTemplate>
        <DataTemplate>

            <Grid>
                <TextBlock x:Name="ValueAvailable" Text="Binding PriceChangeMonthly.Display" HorizontalAlignment="Center" VerticalAlignment="Center"/>
                <Ellipse Width="6" Height="6" x:Name="ValueNotAvailable" Visibility="Hidden" Fill="#5a5a5a"/>
            </Grid>

            <DataTemplate.Triggers>
                <DataTrigger Binding="Binding PriceChangeMonthly.Display" Value="-">
                    <Setter TargetName="ValueAvailable" Property="Visibility" Value="Hidden"/>
                    <Setter TargetName="ValueNotAvailable" Property="Visibility" Value="Visible"/>
                </DataTrigger>
            </DataTemplate.Triggers>

        </DataTemplate>
    </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>

PriceChangeMonthly.Display 跟踪每件商品的价格变化。

MarketData.PriceChangeMonthlyDisplay 对整个集合的这些值求和然后取平均值。

public static decimal PriceChangeMonthly
 get  return MarketData.CopiedCoins?.Where ( c => c.HasBalance ).Select ( c => c.PriceChangeMonthly.InPercent ).DefaultIfEmpty ( ).Average ( ) ?? 0;  

public static string PriceChangeMonthlyDisplay

    get
    
        decimal change = MarketData.PriceChangeMonthly;
        return String.Format ( "01:n1%m", ( change >= 0 ) ? "+" : String.Empty, change );
    

【问题讨论】:

我不太明白你的解释。您能否举一个数据计算的示例和/或输出 DataGrid 表的屏幕截图。首先,我感兴趣的是计算是只依赖于这个集合元素的属性,还是对其他元素有依赖。 是的,它看起来像这样:imgur.com/a/KHgHlYW 你可以使用所有这些值,我只是将它们平均并在列标题中显示该平均值。它们不依赖于其他任何东西。 如果我理解正确,那么您需要在其标题中获取列元素的平均值吗?如果是这样,那么决定关键取决于是否需要在指定集合时一次性计算这个值,或者这个集合是否可以动态变化并且需要实时计算平均值。请说明这个细微差别。 是的,基本上就是你所说的,它必须随着值的变化而更新,所以标题应该始终显示其列元素的平均值。 【参考方案1】:

我给出第二个答案,因为我最初误解了这个问题。

实现了一个代理来监视集合元素的指定属性。 如果它们的值发生变化,或者集合发生变化,则所有新值都将传递给 Values 属性。

using System;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Markup;

namespace Converters

    [ValueConversion(typeof(object), typeof(object[]))]
    public class ToArrayConverter : IValueConverter, IMultiValueConverter
    
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
            => new object[]  value ;

        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        
            return values?.Clone();
        

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        
            throw new NotImplementedException();
        

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        
            throw new NotImplementedException();
        

        public static ToArrayConverter Instance  get;  = new ToArrayConverter();
    

    public class ToArrayConverterExtension : MarkupExtension
    
        public override object ProvideValue(IServiceProvider serviceProvider)
            => ToArrayConverter.Instance;
    

using Converters;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Data;

namespace Proxy

    public class ObservingСollectionItemsProxy : Freezable
    


        /// <summary>
        /// Indexed collection. Items in this collection will be observabled.
        /// </summary>
        public IList List
        
            get  return (IList)GetValue(ListProperty); 
            set  SetValue(ListProperty, value); 
        

        /// <summary><see cref="DependencyProperty"/> for property <see cref="List"/>.</summary>
        public static readonly DependencyProperty ListProperty =
            DependencyProperty.Register(nameof(List), typeof(IList), typeof(ObservingСollectionItemsProxy), new PropertyMetadata(null));



        /// <summary>
        /// The path to the property of the element, the value of which will be passed to the array of values.
        /// </summary>
        public string ValuePath
        
            get  return (string)GetValue(ValuePathProperty); 
            set  SetValue(ValuePathProperty, value); 
        

        /// <summary><see cref="DependencyProperty"/> for property <see cref="ValuePath"/>.</summary>
        public static readonly DependencyProperty ValuePathProperty =
            DependencyProperty.Register(nameof(ValuePath), typeof(string), typeof(ObservingСollectionItemsProxy), new PropertyMetadata(null, PathChanged));

        private string privatePath;
        private static void PathChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        
            ObservingСollectionItemsProxy proxy = (ObservingСollectionItemsProxy)d;
            proxy.privatePath = (string)e.NewValue;
            proxy.PathOrCountChanged();
        



        /// <summary>
        /// An array of observed values.
        /// </summary>
        public IReadOnlyList<object> Values
        
            get  return (IReadOnlyList<object>)GetValue(ValuesProperty); 
            private set  SetValue(ValuesPropertyKey, value); 
        

        private static readonly IReadOnlyList<object> emptyArray = new object[0];

        private static readonly DependencyPropertyKey ValuesPropertyKey =
            DependencyProperty.RegisterReadOnly(nameof(Values), typeof(IReadOnlyList<object>), typeof(ObservingСollectionItemsProxy), new PropertyMetadata(emptyArray));
        /// <summary><see cref="DependencyProperty"/> for property <see cref="Values"/>.</summary>
        public static readonly DependencyProperty ValuesProperty = ValuesPropertyKey.DependencyProperty;




        /// <summary>Private property for creating binding to collection items.</summary>
        private static readonly DependencyProperty PrivateArrayProperty =
            DependencyProperty.Register("1", typeof(object[]), typeof(ObservingСollectionItemsProxy), new PropertyMetadata(null, ArrayChanged));

        private static void ArrayChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        
            ((ObservingСollectionItemsProxy)d).Values = (!(e.NewValue is object[] array) || array.Length == 0)
                ? emptyArray
                : Array.AsReadOnly(array);
        

        /// <summary>A private property to monitor the number of items in the collection.</summary>
        private static readonly DependencyProperty PrivateCountProperty =
            DependencyProperty.Register("2", typeof(int?), typeof(ObservingСollectionItemsProxy), new PropertyMetadata(null, CountChanged));


        private int? privateCount;
        private static void CountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        
            ObservingСollectionItemsProxy proxy = (ObservingСollectionItemsProxy)d;
            proxy.privateCount = (int?)e.NewValue;
            proxy.PathOrCountChanged();
        

        private void PathOrCountChanged()
        
            MultiBinding multiBinding = new MultiBinding()  Converter = ToArrayConverter.Instance ;
            if (privateCount != null && privateCount > 0)
            
                string path = string.IsNullOrWhiteSpace(privatePath)
                    ? string.Empty
                    : $".privatePath.Trim()";
                for (int i = 0; i < privateCount.Value; i++)
                
                    Binding binding = new Binding($"nameof(List)[i]path")
                    
                        Source = this
                    ;
                    multiBinding.Bindings.Add(binding);
                
            
            BindingOperations.SetBinding(this, PrivateArrayProperty, multiBinding);
        

        public ObservingСollectionItemsProxy()
        
            Binding binding = new Binding($"nameof(List).nameof(List.Count)")
            
                Source = this
            ;
            _ = BindingOperations.SetBinding(this, PrivateCountProperty, binding);
        

        protected override Freezable CreateInstanceCore()
        
            throw new NotImplementedException();
        

    


要处理生成的值集合,您可以使用转换器

在处理值的函数的参数中接收委托的转换器示例。 对于此任务,为了让转换器处理值,委托参数必须与 IReadOnlyList&lt;object&gt; 兼容。 也就是说,可以有以下类型中的一种:IReadOnlyCollection&lt;object&gt;IEnumerable&lt;object&gt;IEnumerable

using System;
using System.Globalization;
using System.Reflection;
using System.Windows;
using System.Windows.Data;
using System.Windows.Markup;

namespace Converters

    public class FuncForValueConverter : IValueConverter
    
        public static FuncForValueConverter Instance  get;  = new FuncForValueConverter();

        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        
            if (parameter is Delegate func)
            
                MethodInfo method = func.Method;

                Type retType = method.ReturnType;
                if (retType != null && retType != typeof(void))
                
                    ParameterInfo[] parameters = method.GetParameters();
                    if (parameters.Length == 1)
                    
                        try
                        
                            return func.DynamicInvoke(new object[]  value );
                        
                        catch (Exception)
                         
                    
                
            

            return DependencyProperty.UnsetValue;
        

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        
            throw new NotImplementedException();
        


    
    public class FuncForValueConverterExtension : MarkupExtension
    
        public override object ProvideValue(IServiceProvider serviceProvider)
            => FuncForValueConverter.Instance;
    

用法示例。

Helper 类:集合项类型、集合类型、函数集。

using Simplified;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;

namespace ItemsProxyTest

    public class DoubleItem : BaseInpc
    
        private double _number;

        public double Number  get => _number; set => Set(ref _number, value); 
    

    public static class Function
    
        public static readonly Func<IReadOnlyList<object>, double> Average 
            = (arr) => arr
            .OfType<double>()
            .Average();

        public static readonly Func<IReadOnlyList<object>, double> Sum
            = (arr) => arr
            .OfType<double>()
            .Sum();
    

    public class DoubleItemCollection : ObservableCollection<DoubleItem>
     

XAML:

<Window x:Class="ItemsProxyTest.ItemsProxyTestWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ItemsProxyTest"
        xmlns:proxy="clr-namespace:Proxy;assembly=Common"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        xmlns:cnv="clr-namespace:Converters;assembly=Common"
        mc:Ignorable="d"
        Title="ItemsProxyTestWindow" Height="450" Width="800">
    <FrameworkElement.Resources>
        <local:DoubleItemCollection x:Key="list">
            <local:DoubleItem Number="123"/>
            <local:DoubleItem Number="456"/>
            <local:DoubleItem Number="789"/>
        </local:DoubleItemCollection>
    </FrameworkElement.Resources>
    <Grid>
        <DataGrid ItemsSource="DynamicResource list"
                  AutoGenerateColumns="False">
            <DataGrid.Columns>
                <DataGridTextColumn Binding="Binding Number">
                    <DataGridColumn.HeaderTemplate>
                        <DataTemplate>
                            <StackPanel>
                                <FrameworkElement.Resources>
                                    <proxy:ObservingСollectionItemsProxy
                                          x:Key="proxy"
                                          List="DynamicResource list"
                                          ValuePath="Number"/>
                                    <sys:String x:Key="str">121321</sys:String>
                                </FrameworkElement.Resources>
                                <TextBlock Text="Binding  Values,
                                    Source=StaticResource proxy,
                                    Converter=cnv:FuncForValueConverter,
                                    ConverterParameter=x:Static local:Function.Average"/>
                                <TextBlock Text="Binding  Values,
                                    Source=StaticResource proxy,
                                    Converter=cnv:FuncForValueConverter,
                                    ConverterParameter=x:Static local:Function.Sum"/>
                            </StackPanel>
                        </DataTemplate>
                    </DataGridColumn.HeaderTemplate>
                </DataGridTextColumn>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>

【讨论】:

【参考方案2】:

我创建了一个通用转换器来评估任何表达式。 我原来的主题:ExpressionConverter. 源代码链接:WpfMvvm/WpfMvvm.Converters NuGet 链接:WpfMvvm.Converters

原文翻译:

ExpressionConverter - 计算简单算术表达式的转换器。 要从转换器中的绑定中获取字符串表达式,请使用 Format(String, Object[]) method。 对于复合格式字符串,使用转换器参数或第一个绑定的值。 要计算字符串中的结果表达式,请使用 DataTable.Compute(String, String) method。 第一个值是接收到的带有表达式的字符串,第二个是空字符串。

转换器可以用作一个值的普通转换器:

        <TextBlock Margin="5" VerticalAlignment="Center" HorizontalAlignment="Center">
            <Run Text="Half window height:"/>
            <Run Text="Binding ActualHeight,
                                ElementName=window,
                                Mode=OneWay,
                                Converter=cnvs:ExpressionConverter,
                                ConverterParameter='0 / 2.0'"/>
        </TextBlock>

它也可以用作多个值的多重转换器:

    <FrameworkElement.Resources>
        <x:Array Type="sys:String" x:Key="operators">
            <sys:String>+</sys:String>
            <sys:String>-</sys:String>
            <sys:String>/</sys:String>
            <sys:String>*</sys:String>
        </x:Array>
    </FrameworkElement.Resources>
    <UniformGrid Columns="1">
        <UniformGrid Columns="5" VerticalAlignment="Center">
            <TextBox x:Name="tb1" Text="1.2" Margin="5" HorizontalContentAlignment="Center"/>
            <ComboBox x:Name="cBox" Margin="5" HorizontalContentAlignment="Center"
                      ItemsSource="DynamicResource operators"
                      SelectedIndex="0"/>
            <TextBox x:Name="tb2" Text="3.4" Margin="5" HorizontalContentAlignment="Center"/>
            <TextBlock Text="=" TextAlignment="Center" Margin="5"/>
            <TextBlock TextAlignment="Center" Margin="5">
                <TextBlock.Text>
                    <MultiBinding Converter="cnvs:ExpressionConverter"
                                  ConverterParameter="(0) 1 (2)">
                        <Binding Path="Text" ElementName="tb1"/>
                        <Binding Path="SelectedItem" ElementName="cBox"/>
                        <Binding Path="Text" ElementName="tb2"/>
                    </MultiBinding>
                </TextBlock.Text>
            </TextBlock>
        </UniformGrid>

P.S.如果您对类似方法感兴趣,如果您提供有关您的任务的更多详细信息,我可以向您展示如何将其用于您的任务。

【讨论】:

CalcBinding 不是这里发明的

以上是关于有没有办法在 WPF 中求和或平均属性?的主要内容,如果未能解决你的问题,请参考以下文章

有没有办法在 wpf WebBrowser 控件之上呈现 WPF 控件?

有没有办法检查 WPF 当前是不是在设计模式下执行?

有没有办法从 C# WPF 应用程序中刷新 DNS 缓存? (在 XP、Vista、Win7 上)

对对象列表的属性求和/平均

WPf:一次绑定多个属性

有没有办法用 .Net 3.5 在 WPF 中模拟 UseLayoutRounding