0
votes

I have a WPF application wherein a Window has a master/child relationship. When a user selects a Buyer from a ListView, a DataGrid is bound with related Items they have purchased. In the GridView, there is a button on the left where a Payment for this individual item can be made. However, I want to disable/enable the button based on the state of the Item, whether or not an existing Payment is associated with it. Using Galasoft MVVM Light, RelayCommand throughout the application. I have the Pay button working, but ONLY when clicked (row selected). This works fine when bound to a command in this way:

<DataGrid ..
  ItemsSource="{Binding Items}" 
  SelectedItem="{Binding SelectedItem}"
  IsSynchronizedWithCurrentItem="True"
  .. >
    <DataGrid.Columns>
      <DataGridTemplateColumn>
        <DataGridTemplateColumn.CellTemplate>
          <DataTemplate>
          <Button ...
           Command="{Binding Path=DataContext.PayItemCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}" 
           CommandParameter="{Binding SelectedItem, Mode=OneWay, ElementName=itemsPurchasedGrid}"
          >Pay</Button>
          ...

// in ctor
PayItemCommand = new RelayCommand<Item>(PayItem, IsItemUnPaid);

// in body
private void PayItem(Item item)
{
    PaymentMode = PaymentMode.Item;
    Messenger.Default.Send<BuyerViewModel>(this, "LaunchPayWindow");
}

// CanExecute impl
private bool IsItemUnPaid(Item item)
{
   // code to determine if item is already paid
}

So, when I click the button, it correctly passes the SelectedItem to the RelayCommand delegate. However, the CanExecute implementation: IsItemUnPaid(Item item) never gets the item, because it is not (yet) selected. I tried: IsSynchronizedWithCurrentItem="True", which always passes the first Item, but not any others. Is there a way to get a handle to the Item while it is binding to the DataGrid to disable/enable it? I'm sure there are ugly ways in the codebehind during binding, but is there an MVVM way to check the current item and toggle that button? Many thanks!

P.S. I wrote a simple converter which works, but is that really the best way?

public class PayItemConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (!(value is Item item))
            return true;

        // if you find any payments for this item...
        var payments = (item.Payments != null && item.Payments.Any(p => p.ItemId.HasValue && p.ItemId.Value == item.Id))
            ? item.Payments.Where(p => p.ItemId.HasValue && p.ItemId.Value == item.Id).ToArray()
            : null;

        // if you find payments for this item, are the sum of the payments less than the total of the item TotalAmount, it is said to be unpaid
        return (payments != null && payments.Length > 0)
            ? payments.Sum(p => p.Amount) < item.TotalAmount
            : true;                      
    }
1

1 Answers

0
votes

No, a converter is not the best way of implementing this. The whole point of MVVM is to maintain good separation-of-concerns between your view and your view logic. Converters most definitely reside in the view layer; as soon as you find yourself adding application-type logic to a converter it's usually a sign that your view model is not coercing your data into a format that can be readily consumed by the view.

The problem here is that your Items property seems to be model data, presumably from a database or something. What it needs to be is a collection of view models, one per item, which exposes the properties of the item (or even the whole item itself) and also contains extra fields for the logic needed by your view i.e.:

public bool PayButtonEnabled {get; private set;}