다음을 통해 공유


Launching a custom Dialog to edit items in a DataGrid (with MVVM)

Launching a custom dialog for editing on the DataGrid is another somewhat common request that I see from the discussion list. I thought I would provide a sample but at the same time implement it with the MVVM pattern. The requirement that I will use on the sample is that the editing mechanism can only come from the editing dialog that I provide.

I decided that the way to launch the custom dialog will be through double-clicking on the row. To get that behavior, I will use the attached behavior pattern on DataGridRow. The behavior is defined like so:

public class DataGridRowBehavior

{

  public static DependencyProperty OnDoubleClickProperty = DependencyProperty.RegisterAttached(

      "OnDoubleClick",

      typeof(ICommand),

      typeof(DataGridRowBehavior),

      new UIPropertyMetadata(DataGridRowBehavior.OnDoubleClick));

  public static void SetOnDoubleClick(DependencyObject target, ICommand value)

  {

      target.SetValue(DataGridRowBehavior.OnDoubleClickProperty, value);

  }

  private static void OnDoubleClick(DependencyObject target, DependencyPropertyChangedEventArgs e)

  {

      var element = target as DataGridRow;

      if ((e.NewValue != null) && (e.OldValue == null))

      {

        element.PreviewMouseDoubleClick += OnPreviewMouseDoubleClick;

      }

      else if ((e.NewValue == null) && (e.OldValue != null))

      {

        element.PreviewMouseDoubleClick -= OnPreviewMouseDoubleClick;

      }

  }

  private static void OnPreviewMouseDoubleClick(object sender, MouseButtonEventArgs e)

  {

      UIElement element = (UIElement)sender;

      ICommand command = (ICommand)element.GetValue(DataGridRowBehavior.OnDoubleClickProperty);

      command.Execute(e);

  }

}

 

The behavior is then used on the DataGridRow like this:           

<Style TargetType="{x:Type toolkit:DataGridRow}">

  <Setter Property="local:DataGridRowBehavior.OnDoubleClick"

          Value="{Binding ShowEditItemDialogCommand}" />

</Style>

 

What’s happening is the OnDoubleClick attached property listens for a property change on itself. I define an ICommand, ShowEditItemDialogCommand, on OnDoubleClick which then hooks up the actual event handler, PreviewMouseDoubleClick, on the target element. In that event handler, OnPreviewMouseDoubleClick, I retrieve the value of OnDoubleClick which is my ICommand and execute it. So now we have the behavior to launch a dialog through a double-click action. The next thing is to define the ICommand ShowEditItemDialogCommand.

I have defined a DataGridRow ViewModel which hosts the actual item data as well as any properties that will drive the view (the DataGridRow). It also will host any commands on a DataGridRow which for our purposes will be the ShowEditItemDialogCommand. My commands are also implemented using the RelayCommand mechanism that is discussed on this MSDN article. So here is the basic implementation:                           

public class ItemViewModel : ViewModelBase

{

  private ICommand _editItemCommand;

  public ICommand ShowEditItemDialogCommand

  {

  get

{

      if (_editItemCommand == null)

{

        _editItemCommand = new RelayCommand(

        () =>

           {

                 // pass the item to the dialog

                 var dlg = new EditItemDialog(Item);

                 if (dlg.ShowDialog() == true)

                 {

                 // retrieve the updated item from the dialog

                 Item = dlg.Result.Item;

                 }

           });

      }

      return _editItemCommand;
}

  }

}

 

So when DataGridRowBehavior.OnPreviewMouseDoubleClick executes the command, my ShowEditItemDialogCommand will initiate the launch of the new dialog. The dialog that is being launched is basically a new Window which is considered a View. From a MVVM purist perspective this does not belong in a ViewModel. To handle this you can use the mediator design pattern to host the communication between the ViewModel and the View. There are a couple ways to implement this pattern which include implementing it as an observer pattern or using a specialized communication mechanism through interfaces and references. I’ve chosen the latter and in the sample code you can see I’ve named it EditDialogDirector to be clear on its behavior. I won’t show all the details here.

Now, I said earlier that I set a requirement that editing can only come from the dialog that I provide. While there are a lot of ways to customize the editing behavior, I decided to keep it really simple and set DataGrid.IsReadOnly to true. It does mean that I will lose the default editing capabilities that DataGrid provides, but I won’t have to keep state on DataGrid begin and commit edits. There are some other details that I’m leaving out but the full sample code below will give you a good idea of how everything is pieced together. Also, there is a lot of great reference material and samples on MVVM to further enhance your knowledge on the subject.

https://blogs.msdn.com/johngossman/archive/2005/10/08/478683.aspx

https://karlshifflett.wordpress.com/mvvm/

https://www.codeproject.com/KB/WPF/InternationalizedWizard.aspx

Download the full sample here.

 

 

DataGridMVVMEditingSample.zip

Comments

  • Anonymous
    April 10, 2009
    Thank you for submitting this cool story - Trackback from DotNetShoutout

  • Anonymous
    April 13, 2009
    Lorsque l’on travaille avec WPF et le pattern MVVM (Model View ViewModel), on essaye d’éviter au maximum

  • Anonymous
    April 21, 2009
    Vince, your blog is great. A variant of this pattern is to put the behavior on the grid itself, using the VisualTreeHelper to determine where the doubleclick on the grid actually occured. This eliminates the need for a style on the row.

  • Anonymous
    April 22, 2009
    The comment has been removed

  • Anonymous
    April 23, 2009
    Dave, I believe this was a known issue with the Cider folks and has been recently fixed. http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=400222

  • Anonymous
    April 28, 2009
    I like the idea of using a command on the view model. However, what I do within the view model is define an event when I want an entity to be edited. The GUI will then listen for that event (which includes in the args an instance of the model to be used to maintain that entity) and do the appropriate task (which may show a dialog or use another part of the screen to edit it, etc).

  • Anonymous
    April 28, 2009
    The comment has been removed

  • Anonymous
    May 11, 2009
    I don't want to sound pedantic here, but isn't your ViewModel here coupled with the UI?  Just calling dlg.ShowDialog() shows that your ViewModel is aware of the UI.  Shouldn't the ViewModel be entirely decoupled from the user interface?  How would you unit test the edit functionality?  In many of the examples that I saw, people had elegant ways to attach the UI to the ViewModel using things like DataTemplates with DataTypes (http://tinyurl.com/d75cg3)... Is there any way to load up an edit dialog with your viewmodel completely uncoupled?  Don't get me wrong, you solution looks great, but I have been googling for better.. Am I missing something here?  I am pretty new to the MVVM pattern.  Thanks!

  • Anonymous
    May 11, 2009
    Sorry, read all of the comments at the bottom of your article and see you addressing my question =D My apologies for being a lazy reader.

  • Anonymous
    May 12, 2009
    So I have gone over more of the code and I am wondering how you could set up unit tests for your edits? Could you put in some kind of #if directive for the unit test so that it doesn't really open up the window, but instead lets you interact with the mediator, the edit viewModel and the data that is bound to it?  I can kind of see how that would work.  What are your thoughts?  Anybody?  

  • Anonymous
    May 13, 2009
    Jonathan, I've not looked at the specific code in the example, but the general idea is that in unit tests the normal code isn't registered with the mediator, and instead you register test specific code that simulates the editing.  Using a #if directive is certainly NOT the answer to any testing requirements. As an alternative to the mediator, you could use Service Location. Check out Onyx (http://www.codeplex.com/wpfonyx) and how it does this, particularly with the common dialog provider.

  • Anonymous
    May 13, 2009
    What do you mean by "normal code"?  Hmm, if you are unit testing your VM I assume that you usually simulate Commands, right?  In this example, when they call the ShowEditItemDialogCommand, the Mediator is instantiated, which then handles the UI.  Would you then not simulate this Command during unit testing and instead just manually create the Edit VM?

  • Anonymous
    May 13, 2009
    The "normal code" is the code that is run by the mediator when the actual program is run.  Again, I've not looked at this specific code sample, but a mediator is a go between. The VM executes some method or invokes on the mediator, which in turn executes some code registered with the mediator (EventAggregator in Prism is a mediator based on .NET events, so the VM raises an event on the mediator, which in turn invokes any handlers registered on the event).  In a unit test, there's nothing registered with the mediator by default, making it a NOOP when executed by the VM. To test results, you register specific test handlers with the mediator.  Does this make sense?

  • Anonymous
    May 13, 2009
    Bill, Jonathan, The mediator example that I'm showing here doesn't follow the observer pattern so the unit testing would be slightly different.  From the sample, here are the basic pieces:

  1. ItemViewModel has a Command, ShowEditItemDialogCommand.
  2. When executed calls the mediators ShowDialog.
  3. The mediator has access to the actual dialog window and will call Window.ShowDialog
  4. When the actions in the dialog are complete, control flow goes back to the ShowEditItemDialogCommand and properties on the ItemViewModel are updated. Unlike frameworks like Prism and Onyx which offer are way more rich, extensible, plug-n-play experience, using the mediator strategy above is still slightly coupled to the UI.  If you really want to unit test this without touching the View, you will need to create some hooks to simulate the command action (sort of what Bill is saying).  To be honest though, I don't see it as a big problem where some unit tests will be dealing with a View.  In this case after you launch the view dialog, once you have access to the ViewModel that represents the dialog then you can continue with the ViewModel automation.
  • Anonymous
    August 13, 2009
    Vincent- Thank you for the post; this really helped me get started with my experimentation with MVVM and the DataGrid. In my situation, I did not mess with attached behaviors (much more graceful than mine) but simply used a button in DataGridRowHeader template that fires a delegated ICommand launching the modal dialog. This means GridViewModel has a _selectedItem field the grid binds to, and is responsible for creating a Window object and setting it's context - not great as a purist like you pointed out, but not a huge set back in my small app. I am stuck in one situation however and need your advice. To elaborate, my main window displays my items in a DataGrid, who's own view model implements three main pieces: 1. OC<ParentItemViewModel>, 2. SelectedItem property, 3. OnOpenModalDialog() ICommand delegate. So the user selects a row and clicks the edit button. Up comes a new window tied to the SelectedItem ParentItemViewModel. The window is form-like, displaying most properties in editable text boxes; the one exception is its OC<ChildItemViewModel> that is bound to another DataGrid object. This DataGrid is editable unlike the one in our main window. The window is closed by the Ok and Cancel buttons at the bottom. All my validation mechanics and persistence mechanics are working properly - save one thing related to the modal window's DataGrid displaying the ChildItemViewModels: If I close the window (ex. Cancel) while a DataGridRow is still in edit (ex. cell fails validation, outlined red) the window closes fine. No persistence is fired, we return to the main window data grid... but... when I try to edit the same item (select the same DataGridRow and launch it's ItemViewModel in the modal dialog again) I get an InvalidOperationException on the Window.ShowDialog(). There error message that follows is: 'DeferRefresh' is not allowed during an AddNew or EditItem transaction. Any thoughts on that? I'm not quite sure how to ensure the DataGrid ends edit on the button clicks since the ViewModel commands should be completely unaware of how views are implemented. Thank you very much for your help. trey . white @ fire . ca . gov

  • Anonymous
    September 25, 2009
    at the risk of asking a stupid question... my view is complex, and contains more than one datagrid, but state is shared across the model with the grids. I want to define an command on the viewmodel rather than on the specific items that the datagrid is binding to (to be clear, the viewmodel contains a collection that the grid binds to). I also dont particularly want to chain events from the collection of items up to the viewmodel, but rather call the viewmodel command directly from the grid (unless that is the only elegant option). Any thoughts on this?

  • Anonymous
    October 20, 2009
    Nick, Defining a command on a ViewModel higher than at the item level is fine.  I'm entirely clear on this issue with it.  Maybe you can expand on this further.

  • Anonymous
    November 26, 2009
    The comment has been removed

  • Anonymous
    July 01, 2010
    Simply awesome tip! Thanks