我已经在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
答案 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 < 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;
}