Share ViewModel between Views

I am using XLabs to do my MVVM.
I have a page with a listview. When I select a row I push to a new page. In my head it would be obvious to share the same ViewModel between the two views/pages. That way I already have the data I need in the page I push to, and if I pop back the data in the list is updated as well.

ViewFactory seems to only bind one View-ViewModel at a time?

Best Answer

  • adamkempadamkemp US mod
    Accepted Answer

    Ok, the first thing I did was make your model useful by moving the field you care about into it and making it implement INotifyPropertyChanged (by inheriting from ObservableObject):

    public class TestModel : ObservableObject
    {
        private string _name;
    
        public string Name
        {
            get { return _name; }
            set { SetProperty(ref _name, value); }
        }
    }
    

    Then I changed both of your view models to have a Model property instead of each having a separate string property:

        private TestModel _model;
    
        public TestModel Model
        {
            get { return _model; }
            set { SetProperty(ref _model, value); }
        }
    

    Then I changed the bindings for the Entry on each page to refer to Model.Name instead of the (now-removed) string property on the view model:

    test1Entry.SetBinding(Entry.TextProperty, "Model.Name");
    

    Lastly, I had to initialize the Model property. I did that in two places. First, in App when creating the first page:

    MainPage = new NavigationPage((Page)ViewFactory.CreatePage<Test1ViewModel, Test1View>((viewModel, page) => viewModel.Model = new TestModel()));
    

    The new part here is the lambda inside the call to CreatePage. That is a callback that is called after the view model and page are created so that you can do extra initialization. In this case I am just setting the Model property of the view model to a new instance of TestModel. That will be our one-and-only model, shared among view models.

    Then, when navigating to the second page we have to pass that same model element to the new view model. I do that in a similar way:

    return _pushCommand ?? (_pushCommand = new RelayCommand(() => Navigation.PushAsync<Test2ViewModel>((viewModel, page) => viewModel.Model = Model)));
    

    In this case I'm again using a callback argument in PushAsync to copy the Model property from this view model into the new view model. That way I don't need a singleton.

    Attached is a modified version of what you sent me with all of these changes, and you can see that changes on either page are reflected in the other page. (Tip for the future: if you do a "clean" and then delete the folders in the "packages" directories before zipping then you can drastically reduce the size, and all of that stuff can just be restored when the solution is opened).

Answers

  • adamkempadamkemp USInsider, Developer Group Leader mod

    The purpose of a view model is to encapsulate the state and logic for a view. If you have two pages with identical state and logic then why are they two pages instead of one? It's more likely that you have some state and some logic that is specific to each page, in which case you should have two view model types.

    What you may be seeing is the need for the third layer: the model. The model is view-independent, and is often shared between view models. Maybe you should have a model class that is accessible through each of the view models.

  • ChaseFlorellChaseFlorell CAInsider, University mod

    If the page is the same, but the data is different, just update the data for the same page.

  • ChrisNielsenChrisNielsen DKMember

    @adamkemp
    So when I tab on my list, I have to send the instance of my model or some kind of identifier to get the right data from a new model instance on the next page? Is there some nice way to do this in MVVM?

    There are some things that the 2 pages have in common. My thought was to share some of that functionality, but I see that it could be messy if one function should behave a little different on one page then the other.

  • adamkempadamkemp USInsider, Developer Group Leader mod

    So when I tab on my list, I have to send the instance of my model or some kind of identifier to get the right data from a new model instance on the next page?

    When you create a new page you give it the view model by setting it as the new page's BindingContext. That view model can have a reference to the same instance of the model.

  • ChrisNielsenChrisNielsen DKMember

    There's something I missed.

    I am first binding my ViewModel to my View/Page.
    From my ViewModel I am creating a new instance of the Model I need.
    When I push from one ViewModel to another I can't send the Model Instance because the PushAsync is not taking any parameters.

    Should I make the Model static so I could access the same instance from different ViewModels?
    Should I bind the ViewModel and the Model, like we bind the ViewModel and the View? If yes, how?

  • ChrisNielsenChrisNielsen DKMember

    Or should I use Ioc?

  • NMackayNMackay GBInsider, University mod

    @ChrisNielsen

    That sample I sent you uses a NavigationService wrapper to pass an object to the view contractor, the VM is bound view 1st by using the ViewModelLocator.

    public RelayCommand<Vessel> SelectCommand { get { return _selectCommand ?? (_selectCommand = new RelayCommand<Vessel>( ves => { try { if (!_selectCommand.CanExecute(ves)) { return; } _navService.NavigateTo(ViewModelLocator.PageKeyVesDetails, ves);

    That class and interface need to be altered at some stage to make them awaitable.

  • adamkempadamkemp USInsider, Developer Group Leader mod

    PushAsync takes a Page. That Page has to be constructed somehow, and when it is constructed it should be given a view model. That view model has to be constructed somehow too, and when that happens you can give it your model. If this is difficult to do then you may need to rethink your design because this really shouldn't be difficult, and it shouldn't require any singletons.

  • ChrisNielsenChrisNielsen DKMember

    @NMackay I found out taht XLabs already have a NavigationService

    I am trying just to navigate without a parameter, but the _nav is never set at line 48 and that make it throw a exception at line 102 Do I need to set something in the iOS project?

    @adamkemp Maybe I am using something wrong but I can't figure out to use the forms push with ViewFactory and parameters, if that's what you mean? Can you make a simple example? I am trying to "share" a model instance and not just a model between two ViewModels.

  • adamkempadamkemp USInsider, Developer Group Leader mod

    I don't know what your code looks like. Apparently you're using Labs for navigation? Could you share an example?

  • ChrisNielsenChrisNielsen DKMember

    In ViewModel A I make an instance of a Model. Let's say the Model have a property called name

    public class Model
    {
            public string name { get; set; }
    }
    

    Now a want to use that instance in another ViewModel (ViewModel B ).

    The way I would do it normally would be to send the instance as a parameter when I instantiate ViewModel B.

    When I navigate I do it like this
    Navigation.PushAsync<RandomViewModel>();

    I don't know how to send the instance to RandomViewModel like this...

  • adamkempadamkemp USInsider, Developer Group Leader mod

    I meant share a zip of a full sample solution so I can see and run the code. I don't know what that navigation API is since it's not stock Xamarin.Forms.

  • ChrisNielsenChrisNielsen DKMember
    edited April 2015

    To be clear. In this example I would have used ViewFactory to bind the two ViewModels to there Views

    ViewFactory.Register<View, ViewModel>();

  • ChrisNielsenChrisNielsen DKMember

    @adamkemp Thank you so much. I have been trying to figure this out all day and my head is about to explode
    I am sending you a PM with the project

  • adamkempadamkemp USInsider, Developer Group Leader mod
    Accepted Answer

    Ok, the first thing I did was make your model useful by moving the field you care about into it and making it implement INotifyPropertyChanged (by inheriting from ObservableObject):

    public class TestModel : ObservableObject
    {
        private string _name;
    
        public string Name
        {
            get { return _name; }
            set { SetProperty(ref _name, value); }
        }
    }
    

    Then I changed both of your view models to have a Model property instead of each having a separate string property:

        private TestModel _model;
    
        public TestModel Model
        {
            get { return _model; }
            set { SetProperty(ref _model, value); }
        }
    

    Then I changed the bindings for the Entry on each page to refer to Model.Name instead of the (now-removed) string property on the view model:

    test1Entry.SetBinding(Entry.TextProperty, "Model.Name");
    

    Lastly, I had to initialize the Model property. I did that in two places. First, in App when creating the first page:

    MainPage = new NavigationPage((Page)ViewFactory.CreatePage<Test1ViewModel, Test1View>((viewModel, page) => viewModel.Model = new TestModel()));
    

    The new part here is the lambda inside the call to CreatePage. That is a callback that is called after the view model and page are created so that you can do extra initialization. In this case I am just setting the Model property of the view model to a new instance of TestModel. That will be our one-and-only model, shared among view models.

    Then, when navigating to the second page we have to pass that same model element to the new view model. I do that in a similar way:

    return _pushCommand ?? (_pushCommand = new RelayCommand(() => Navigation.PushAsync<Test2ViewModel>((viewModel, page) => viewModel.Model = Model)));
    

    In this case I'm again using a callback argument in PushAsync to copy the Model property from this view model into the new view model. That way I don't need a singleton.

    Attached is a modified version of what you sent me with all of these changes, and you can see that changes on either page are reflected in the other page. (Tip for the future: if you do a "clean" and then delete the folders in the "packages" directories before zipping then you can drastically reduce the size, and all of that stuff can just be restored when the solution is opened).

  • ChrisNielsenChrisNielsen DKMember

    @adamkemp Thank you so much!!!
    You rock!

  • abraabra ADMember ✭✭✭

    Is this still the recommended way of creating pages (views) from a view-model ?
    How can this be adapted in case that the pages (views) are located in a shared project, and the view-models in a .NET standard library ?

Sign In or Register to comment.