如何在WPF DataGrid中实现运行平衡列?

时间:2018-12-20 20:54:42

标签: c# wpf datagrid

我已经在Google上搜索了两天,找不到可以使用的答案。 在其他所有财务应用程序中,这都是一个简单的“运行平衡”。我发现的东西要么是总计(最后是一个总数),要么是对PropertyChanged的反应(我的网格不可直接编辑),或者是一半的答案(“使用CollectionView”,但不要说怎么做,我'没看到)。

我如何将ObservableCollection绑定到DataGrid并保持“运行余额”作为计算列(而不是模型的一部分)而在其中一个列上幸存下来?

(编辑)我正在寻找的示例

    Date    Payment    Deposit    Balance
09/01/2018     0.00    1500.00    1500.00
10/01/2018   100.00       0.00    1400.00
11/01/2018   234.00       0.00    1166.00
12/01/2018   345.00       0.00     821.00

...或重新排序后...

    Date    Payment    Deposit    Balance
12/01/2018   345.00       0.00    -345.00
11/01/2018   234.00       0.00    -579.00
10/01/2018   100.00       0.00    -679.00
09/01/2018     0.00    1500.00     821.00

3 个答案:

答案 0 :(得分:0)

您可以在第一个旁边尝试单独的DataGrid。

答案 1 :(得分:0)

我想我理解你的困境。您希望运行余额显示特定交易对期初余额的影响,但该运行余额必须考虑先前的交易。我认为this article很好地总结了您的意图(没有双关语)?

将一列绑定到与交易模型分开的该计算将是有问题的。 DataGrid并非旨在绑定到多个数据源。这个想法是网格中的一行代表一个数据集。您也许可以使用sort事件来发挥创造力,然后逐行读取当前值并以这种方式进行计算,但我认为这并不是最好的解决方法。

相反,您可以将运行余额作为模型的属性,但是在将事务加载到可观察集合中时进行计算。这适用于您的方案,因为您说用户不直接通过网格进行编辑。因此,您可以在将事务添加到ObservableCollection之前对其进行转换。

如果要从数据库加载事务或从文件反序列化,只需将该属性标记为“未映射”,或使用诸如AutoMapper之类的功能将事务模型映射到事务ViewModel。

尽管我使用其后的代码编写了该示例,但由于它不直接引用任何ui组件,因此可以在MVVM中轻松完成。

也许这样会起作用?:

 <Window x:Class="WpfApp2.MainWindow"
        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:WpfApp2"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Grid.RowDefinitions>
        <RowDefinition Height="*"/>
      <RowDefinition Height="Auto"/>
      </Grid.RowDefinitions>
        <DataGrid x:Name="MyDataGrid" ItemsSource="{Binding Transactions}" AutoGenerateColumns="False">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Date" Binding="{Binding Date}"/>
                <DataGridTextColumn Header="Amount" Binding="{Binding Amount}" />
                <DataGridTextColumn Header="Running Balance" Binding="{Binding RunningBalance}"/>
            </DataGrid.Columns>
        </DataGrid>
        <StackPanel Orientation="Horizontal" Grid.Row="1" Margin="5" >
            <Button x:Name="btnAddItem" Content="Add" Width="40" Height="30" Click="BtnAddItem_Click"/>
        </StackPanel>
    </Grid>
</Window>
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;

namespace WpfApp2
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        readonly Random _random;

        public MainWindow()
        {
            InitializeComponent();

            _random = new Random();

            DataContext = this;

            Transactions = new ObservableCollection<Transaction>();
            // add some transactions to the collection to get things started
            AddTransaction(new Transaction()
                {
                    Date = DateTime.Now.Subtract(TimeSpan.FromDays(5)),
                    Amount = -35.66M
                });

            AddTransaction(new Transaction()
                {
                    Date = DateTime.Now.Subtract(TimeSpan.FromDays(4)),
                    Amount = -22.00M
                });

            AddTransaction(new Transaction()
                {
                    Date = DateTime.Now.Subtract(TimeSpan.FromDays(3)),
                    Amount = -10.10M
                });
        }

        /// <summary>
        /// All transactions are added to the collection through this method so that the running balance
        /// can be calculated based on the previous transaction
        /// </summary>
        /// <param name="transaction"></param>
        void AddTransaction(Transaction transaction)
        {
            //find the preceding transaction
            var precedingTransaction = Transactions.Where(t => t.Date &lt; transaction.Date)
                .OrderByDescending(t => t.Date)
                .FirstOrDefault();

            if(precedingTransaction == null)
            {
                //This is the earliest transaction so calc based on starting balance
                transaction.RunningBalance = StartingBalance + transaction.Amount;
            } else
            {
                //this is not the earliest transaction so calc based on previous
                transaction.RunningBalance = precedingTransaction.RunningBalance + transaction.Amount;
            }

            //Add the transactions to the collection with the calculated balance
            Transactions.Add(transaction);
        }

        void BtnAddItem_Click(object sender, RoutedEventArgs e)
        {
            AddTransaction(new Transaction()
                {
                    Date = DateTime.Now,
                    //generate a random dollar amount
                    Amount = (decimal)-Math.Round(_random.Next(1, 100) + _random.NextDouble(), 2)
                });
        }

        public decimal StartingBalance => 345.00M;

        public ObservableCollection<Transaction> Transactions { get; set; }
    }

    public class Transaction
    {
        public decimal Amount { get; set; }

        public DateTime Date { get; set; }

        public decimal RunningBalance { get; set; }
    }
}

答案 2 :(得分:0)

Started by realizing this is presentation logic, so it belongs in the View. Realized what I should do is reorder the ObservableCollection and establish the running total at that time (which led me here).

But I still couldn't get the ObservableCollection to refresh. If I replaced it with a new (sorted) ObservableCollection that broke the binding logic. So I went and found this answer which eventually led me to this GitHub.

With the new class in place the xaml.cs turns into this:

private void DataGrid_OnSorting(object sender, DataGridSortingEventArgs e)
{
    decimal runningTotal = 0.0M;
    //I have to maintain the sort order myself. If I let the control do it it will also resort the items again
    e.Column.SortDirection = e.Column.SortDirection == ListSortDirection.Ascending ? ListSortDirection.Descending : ListSortDirection.Ascending;

    IEnumerable<RegisterEntry> tempList = RegisterList;

    switch (e.Column.Header.ToString())
    {
        case "Payment":
            tempList = e.Column.SortDirection == ListSortDirection.Ascending ? tempList.OrderBy(item => item.Payment) : tempList.OrderByDescending(item => item.Payment);
            break;
        case "Transaction":
            tempList = e.Column.SortDirection == ListSortDirection.Ascending ? tempList.OrderBy(item => item.TransactionDate) : tempList.OrderByDescending(item => item.TransactionDate);
            break;
        case "Payee":
            tempList = e.Column.SortDirection == ListSortDirection.Ascending ? tempList.OrderBy(item => item.itemPayee) : tempList.OrderByDescending(item => item.itemPayee);
            break;
    }

    tempList = tempList
        .Select(item => new RegisterEntry()
        {
            Id = item.Id,
            AccountId = item.AccountId,
            TransactionDate = item.TransactionDate,
            ClearanceDate = item.ClearanceDate,
            Flag = item.Flag,
            CheckNumber = item.CheckNumber,
            itemPayee = item.itemPayee,
            itemCategory = item.itemCategory,
            Memo = item.Memo,
            itemState = item.itemState,
            Payment = item.Payment,
            Deposit = item.Deposit,
            RunningBalance = (runningTotal += (item.Deposit - item.Payment))
        }).ToList();

    RegisterList.ReplaceRange(tempList);

    // Set the event as Handled so it doesn't resort the items.
    e.Handled = true;
}