MVVM - Can the ViewModel Manipulate the View?

MVVM - Can the ViewModel Manipulate the View?

Old forum URL: forums.lhotka.net/forums/t/9086.aspx


Smith866 posted on Tuesday, June 15, 2010

I have a design question regarding the MVVM pattern.  Let me briefly describe my problem so you can see where I'm coming from:

I have an Editable Root Collection that I am loading into a Silverlight DataGrid (The standard Silverlight Toolkit grid, not a 3rd party).  I have added an "Add" button to my page that allows me to add records to my editable root list.  The Add button is wired up to the AddNewCore function and everything works fine.  The issue is that the grid is rather small, and sometimes you need to scroll down to see all of the items.  In this case, when you press the Add button, it appears to the user that nothing has happened.  In reality, the AddNewCore function has been called and there is a blank row waiting for them down at the bottom of the grid, if they happen to know to scroll down to find it.

I need to make my Add button scroll the grid down to the bottom and put focus in one of the columns in my new row.  This isn't overly hard to do, but what I'm wondering is how this should be accomplished in the MVVM world.  I don't want to use code-behind, as I'm pretty sure that should be avoided as much as possible.  What I ended up doing was this:  I passed my DataGrid as a parameter to my ViewModel when the Add button was pressed.  The ViewModel can then attempt to manipulate the DataGrid as desired after the new row has been added.

This all seems to work just find, but I'm just wondering if I have violated some MVVM principal by doing this?  Is there a better solution?  Should this kind of thing be handled by modifying the control itself?

I have attached my ViewModel code below:

public override void AddNew(object sender, ExecuteEventArgs e)
        {

            base.AddNew(sender, e);

            //This will select the newly added row, and then scroll the DataGrid there as well.
            var myDataGrid = e.MethodParameter as DataGrid;

            if (myDataGrid != null)
            {
                myDataGrid.SelectedIndex = Model.Count - 1;
                myDataGrid.ScrollIntoView(Model[Model.Count - 1], myDataGrid.Columns[0]);
                myDataGrid.BeginEdit();
            }
        }

cds replied on Tuesday, June 15, 2010

I see your difficulty, but what you've done does violate the MVVM pattern - as I understand it, the View should bind to the ViewModel - the ViewModel shouldn't be aware of the View - you've now tightly bound your ViewModel to a specific implementation of the View - in that a DataGrid control will (or at least should - you've got a null reference check there!) be present.

The correct way to do this would be to expose a property on the ViewModel for the current object and then have the DataGrid bind to that, scrolling as it needs to to ensure that current object was visible.

Of course, whether you can make the native Silverlight DataGrid do that is another matter, though perhaps you can accomplish it by subclassing or through an attached property.

Anyway, that's the theory - good luck putting it into practise! Big Smile

paupdb replied on Tuesday, June 15, 2010

If you insert your new record as the first element of your list, it will appear at the top of the DataGrid - at least thats what I recall.

AFAIK, the DataGrid reflect the exact order of it's ItemsSource elements.

 

So if my vague recollection is correct, then no need to do anything special :)

bartol replied on Wednesday, June 16, 2010

Hi,

This appears to be one of those cases where the limitations of the controls in the view do not allow a "clean" MVVM approach. It is important to remember that the goal of MVVM is not to eliminate code behind but to decouple the View from its data. Your solution breaks this goal. I suggest one of the the solutions below:

1. If the datagrid offers a record added event then handle that event and call ScrollIntoView in there. 

2. Create an interface (IViewNewRecord) with a single method (NewRecordAdded) that is implemented by the view. You have to somehow pass a reference to this interface to the ViewModel (wherever you construct it) and then call the method whenever a new record is added. On the View side just call ScrollIntoView. This is a solution taken from the MVP pattern.

3. Create an event in the ViewModel that is raised whenever a new record is added. This event is handled by the view where the ScrollIntoView is called. You will need to implement IDisposable in the View to get rid of the delegate reference when the View is closed.

Either of these methods will keep your data independent of your view allowing you to migrate to a better grid at some point in the future or replace the grid with a completely different control if required. Sure, they all require code in the code-behind file but they are better compromises than having a reference to the datagrid in the ViewModel.

 

Copyright (c) Marimer LLC