views:

196

answers:

2

First attempt at implementing MVVM pattern on line-of-business project. I am running into questions that I assume there are eaiser answers for like this one:

Protype window is basic master-detail view of a list of items. (a list of Person objects). The view contains an Infragistics xamDataGrid for the master list. As the item is selected in the grid you can edit the details in the detail panel below and presto, as you tab off the fields in the details panel, the updates are shown “real-time” in the grid data. Only thing is I don’t want “presto”, I want “wait till I press the 'Apply Changes' button”.

I was hoping to avoid creating a separate instance of the list to seperate out the master list from the working set of items that I am adding/deleting/modifying in the details panel.

The path I have gone down:

I overrode the CellValuePresenter Style in the grid field so I can set the binding to “OneWay”. This prevents the real-time update.

<ControlTemplate TargetType="{x:Type igDP:CellValuePresenter}">
  <ControlTemplate.Resources>
    <Style TargetType="TextBlock">
      <Setter Property="Background" Value="{Binding Path=DataItem.NameUIProperty.IsDirty, Converter={StaticResource BooleanBrushConverter}}" />
      <Setter Property="IsEnabled" Value="{Binding Path=DataItem.NameUIProperty.IsEditable}" />
    </Style>
  </ControlTemplate.Resources>
  <ContentControl>
    <TextBlock Text="{Binding Path=DataItem.Name, Mode=OneTime}" />
  </ContentControl>              
</ControlTemplate>

Then I add an “ApplyUpdates” command (RelayCommand) to my PersonListViewModel. This raises the “PERSON_ITEM_UPDATED” message. I am using VB ports of the MVVM Foundation Messenger and RelayCommand classes.

#Region "ApplyUpdates Command"

Private mApplyUpdatesCommand As New RelayCommand(AddressOf ApplyUpdates)
Public ReadOnly Property ApplyUpdatesCommand() As ICommand
 Get
  Return mApplyUpdatesCommand
 End Get
End Property

Private Sub ApplyUpdates()
 'the changes are already in the object in the list so we don't have to do anything here except fire off the Applied message
 Messages.AppMessenger.NotifyColleagues(Messages.PERSON_ITEM_UPDATED)
End Sub

#End Region

The PersonView registers for the PERSON_ITEM_UPDATED message and rebinds the grid when the message is received.

'In Loaded Event

'register for window messages we care about
Messages.AppMessenger.Register(Messages.PERSON_ITEM_UPDATED, AddressOf OnPersonItemUpdated)

'EventHandler
Private Sub OnPersonItemUpdated()
  PersonGrid.DataSource = Nothing
  PersonGrid.DataSource = mViewModel.List
End Sub

So, that works, but it smells wrong. The view seems to have too much logic in it and the ViewModel is not dictating the state of the UI, the view is.

What am I missing? What method would you use to get the ViewModel to delay publishing the changes to the view?

Update: I am now going down the path of creating a custom ViewModel for the grid (readonly, no Propertychanged notifications) and an editable ViewModel for the detail area. Both VMs would wrap the same business objects, but the ReadOnly version would not publish changes. This would leave the VM in control of when the view updates.

A: 

When declaring your field layout for the infragistics data grid, you can use UnboundField instead of Field. This class exposes the BindingPath and BindingMode properties for the underlying binding. Using this technique, you can get rid of the real time update and you shouldn't need a custom control template.

My thoughts on moving logic to the VM:

Create a one way binding of the grid's DataSource and the nViewModel.List. ApplyChanges can then call: BindingOperations.GetBindingExpressionBase(dependencyObject, dependencyProperty).UpdateTarget(); to force the target property DataSource to refresh. Unfortunately, this ties your VM to the binding but results in no code in your view.

A big problem here is that if you have this delayed binding scenario, ApplyChanges is something that will really need some IoC into the View because only the View would know how to really do the update (be it using Bindings or whatever). In the end, something along the chain will be managing two instances of a list: the instance in the view and the actual instance in the VM. In this particular scenario, the delayed update seems to be a behavior of the View. However, the UpdateChanges command on the VM actually couples that behavior to the VM, in which case, I'd say it makes sense to store the two list instances in your VM.

Hope this helps.

siz
thanks for the Infragistics tip. That will come in handy one day!
TheZenker
A: 

I had a similar problem implementing an Options Dialog in MVVM. You want to be able to let the user edit the properties of your ViewModel, but only commit the changes when they hit apply. I found a reasonable solution. Here's the code for the simplest of the options pads with only one boolean property "Sound":

class PinBallOptionsPad : AbstractOptionsPad
{
    public PinBallOptionsPad()
    {
        Name = "PinBallOptionsPad";
    }

    public override void Commit()
    {
        base.Commit();
        Properties.Settings.Default.Save();
    }

    #region "Sound"

    public bool SoundEdit
    {
        get
        {
            return m_SoundEdit;
        }
        set
        {
            if (m_SoundEdit != value)
            {
                m_SoundEdit = value;
                CommitActions.Add(
                    () => Properties.Settings.Default.Sound = m_SoundEdit);
                CancelActions.Add(
                    () =>
                    {
                        m_SoundEdit = Properties.Settings.Default.Sound;
                        NotifyPropertyChanged(m_SoundEditArgs);
                        NotifyPropertyChanged(m_SoundArgs);
                    });
                NotifyOptionChanged();
                NotifyPropertyChanged(m_SoundEditArgs);
            }
        }
    }
    private bool m_SoundEdit = Properties.Settings.Default.Sound;
    static readonly PropertyChangedEventArgs m_SoundEditArgs =
        NotifyPropertyChangedHelper.CreateArgs<PinBallOptionsPad>(o => o.SoundEdit);

    public bool Sound
    {
        get
        {
            return Properties.Settings.Default.Sound;
        }
    }
    static readonly PropertyChangedEventArgs m_SoundArgs =
        NotifyPropertyChangedHelper.CreateArgs<PinBallOptionsPad>(o => o.Sound);
    #endregion

}

It looks like a lot for one property, but the nice thing is that everything for that entire property is contained within the Sound region. So if you copy and paste that, and do a search and replace, you can create new properties relatively quickly. In order to understand how the CommitActions and CancelActions works, you'll need the AbstractOptionsPad class as well:

public abstract class AbstractOptionsPad : AbstractPad, IOptionsPad
{
    #region " Commit "

    /// <summary>
    /// If overriding this method, make sure to call base.Commit first.
    /// </summary>
    public virtual void Commit()
    {
        foreach (var commitAction in CommitActions)
        {
            commitAction();
        }
        CommitActions.Clear();
        CancelActions.Clear();
    }

    protected IList<Action> CommitActions
    {
        get
        {
            return m_commitActions;
        }
    }
    private readonly IList<Action> m_commitActions = new List<Action>();

    #endregion

    #region " Cancel "

    /// <summary>
    /// If overriding this method, make sure to call base.Cancel first.
    /// </summary>
    public virtual void Cancel()
    {
        foreach (var cancelAction in CancelActions)
        {
            cancelAction();
        }
        CancelActions.Clear();
        CommitActions.Clear();
    }

    protected IList<Action> CancelActions
    {
        get
        {
            return m_cancelActions;
        }
    }
    private readonly IList<Action> m_cancelActions = new List<Action>();

    #endregion

    public event EventHandler OptionChanged;

    protected void NotifyOptionChanged()
    {
        var evt = OptionChanged;
        if (evt != null)
        {
            evt(this, new EventArgs());
        }
    }
}

Here's what the View looks like for this pad:

<DataTemplate DataType="{x:Type local:PinBallOptionsPad}">
    <CheckBox IsChecked="{Binding SoundEdit}">
        <TextBlock Text="Sound"/>
    </CheckBox> 
</DataTemplate>

So it binds to SoundEdit in the options, but the rest of the application can be bound to the Sound property, and be updated based on the NotifyPropertyChanged event.

Scott Whitlock