Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

M-V-VM - Any Examples of using commands in the ViewModel?

Tags:

mvvm

wpf

I've been developing a very large LOB app using my flavor of M-V-VM which I call M-V-MC (Model-View-ModelController), which is a kind of a combination between M-V-C and M-V-VM. I had posted this answer regarding how views get instantiated in M-V-VM to the question "what-are-the-most-common-mistakes-made-in-wpf-development".

Sam made the following comment regarding my answer:

This creates a follow-up-question: how do you create the views? I use RelayCommands to bind actions from the view to the ViewModel, so the view does not even know an action has fired, does not know he should open a new view. Solution: create an event in the VM for the View to subscribe to?

When I originally started M-V-VM development I had this notion that EVERYTHING should live in the ViewModel, and have studied a lot of examples from guys like Josh Smith and Karl Shifflett. However I have yet to come up with a good example of when a command needs to live in the ViewModel.

For instance, let's say I have a ListView that displays Customers, and a button that I click to allow me to edit the currently selected customer. The ListView (View) is bound to a CustomerVM (ViewModel). Clicking the button fires the EditCustomerCommand which opens a popup window which allows me to edit all the properties of the CustomerVM. Where does this EditCustomerCommand live? If it involves opening a window, (UI functionality), shouldn't it be defined in the code-behind of the view? alt text

Does anyone have any examples of when I should define a command in the View versus the ViewModel?

Matthew Wright states below:

New and delete from a list would be good examples. In those cases, a blank record is added or the current record is deleted by the ViewModel. Any action taken by the view should be in response to those events occurring.

So if I click the new button, what happens? A new instance of the CustomerVM is created by the Parent ViewModel and added to it's collection right? So how then would my editing screen get opened? The view should create a new instance of the Customer ViewModel, and pass it in to the ParentVM.Add(newlyCreatedVM) method right?

Let's say I delete a customer record via the DeleteCommand living on the VM. the VM calls into the business layer and tries to delete the record. It can't so it returns a message to the VM. I want to display this message in dialogbox. How does the view get the message out of the command action?

like image 273
Micah Avatar asked Jan 08 '09 16:01

Micah


People also ask

What should a ViewModel contain?

ViewModel objects can contain LifecycleObservers , such as LiveData objects. However ViewModel objects must never observe changes to lifecycle-aware observables, such as LiveData objects.

What is Mvvm command?

Commands are an implementation of the ICommand interface that is part of the . NET Framework. This interface is used a lot in MVVM applications, but it is useful not only in XAML-based apps.


2 Answers

Never thought I'd see myself being quoted in a question.

I pondered this question myself for some time, and made a rather pragmatic decision for my code base:

In my code base, the ViewModel is getting called when actions happen, and I wanted it to stay this way. Additionally I do not want the ViewModel to control the views.

What did I do?
I added a controller for navigation:

public interface INavigation
{
  void NewContent(ViewModel viewmodel);
  void NewWindow(ViewModel viewmodel);
}

This controller does contain two actions: NewContent() does show new content in the current window, NewWindow() creates a new Window, populates it with the content and shows it.
Of course my viewmodels have no clue which view to show. But they do know which viewmodel they want to show, so according to your example when DeleteCommand is executed, it would call the navigation service function NewWindow(new ValidateCustomerDeletedViewModel()) to show a window stating 'the customer has been deleted' (overkill for this simple messagebox, but it would be easy to have a special navigator function for simple messageboxes).

How does the viewmodel get the navigation service?

My viewmodel class has a property for the navigation controller:

public class ViewModel
{
  public INavigation Navigator { get; set; }
  [...]
}

When a viewmodel is attached to a window (or whatever displays the view), the window will set the Navigator property, so the viewmodel can call it.

How does the navigator create the view to the viewmodel?

You could have a simple list which view to create for which viewmodel, in my case I can use simple reflection since the names are matching:

public static FrameworkElement CreateView(ViewModel viewmodel)
{
  Type vmt = viewmodel.GetType();
  // big bad dirty hack to get the name of the view, but it works *cough*
  Type vt = Type.GetType(vmt.AssemblyQualifiedName.Replace("ViewModel, ", "View, ")); 
  return (FrameworkElement)Activator.CreateInstance(vt, viewmodel);
}

Of course the view needs a constructor accepting the viewmodel as parameter:

public partial class ValidateCustomerDeletedView : UserControl
{
  public ValidateCustomerDeletedView(ValidateCustomerDeletedViewModel dac)
  {
    InitializeComponent();
    this.DataContext = dac;
  }
}

How does my window look like?

Simple: my main window does implement the INavigation interface, and shows a start page on creation. See for yourself:

public partial class MainWindow : Window, INavigation
{
  public MainWindow()
  {
    InitializeComponent();
    NewContent(new StartPageViewModel());
  }

  public MainWindow(ViewModel newcontrol)
  {
    InitializeComponent();
    NewContent(newcontrol);
  }

  #region INavigation Member
  public void NewContent(ViewModel newviewmodel)
  {
    newviewmodel.Navigator = this;
    FrameworkElement ui = App.CreateView(newviewmodel);
    this.Content = ui;
    this.DataContext = ui.DataContext;
  }

  public void NewWindow(ViewModel viewModel)
  {
    MainWindow newwindow = new MainWindow(viewModel);
    newwindow.Show();
  }
  #endregion
}

(This does work equally well with a NavigationWindow and wrapping the view into a Page)

Of course this is testable, since the navigation controller can be mocked easily.

I'm not really sure if this is a perfect solution, but it works nicely for me right now. Any ideas and comments are welcome!

like image 54
Sam Avatar answered Nov 02 '22 23:11

Sam


For your delete message box case, I abstract out messageboxes via an interface. Similar to this. I also inject these interfaces for my WPF app.

Constructor

    public MyViewModel(IMessage msg)
    {
      _msg = msg;
    }

Then, in the method delete method on the ViewModel...something like

    public void Delete()
    {
      if(CanDelete)
      {
        //do the delete 
      }
      else
      {
        _msg.Show("You can't delete this record");
      }
    }

This will make it testable, you can plug in a different IMessage implementations that don't actually show a messagebox. Those might just print to the console, for testing purposes. Obviously your WPF app might have an implementation like

public class MessageBoxQuestion : IMessage
{
   public void Show(string message)
   {
     MessageBox.Show(message);
   }
}

Doing this makes testing the different routes(think Yes/No dialogs) very easy and straight forward. You can imagine a delete confirmation. You could either use a concrete instance of the IMessage to return true/false for confirmation or mock out the container during your test.

[Test]
public void Can_Cancel_Delete()
{
  var vm = new ProductViewModel(_cancel);
  ...

}
[Test]
public void Can_Confirm_Delete()
{
  var vm = new ProductViewModel(_yes);
  ...

}

For your other question on when to use the Command, I instantiate the Add New or Details views from the View in question. Just as you have in your example. Views are only instantiated by other Views in our app. I don't use a command in those cases. I do however use the ViewModel properties of the parent view to the child view.

public void Object_DoubleClick(object sender, EventArgs e)
{
  var detailView = new DetailView(ViewModel.Product);
  detailView.Show();
}

Hope this helps!

like image 29
Jab Avatar answered Nov 02 '22 23:11

Jab