locked
Xamarin.Forms MVVM: How to pass parameter to detail viewmodel? RRS feed

  • Question

  • User159321 posted

    We're trying to follow an MVVM pattern as supported by Xamarin.Forms, but have had some trouble figuring out the best way to respect MVVM conventions while navigating from a master page to a detail page.

    Using this as an example: https://developer.xamarin.com/guides/cross-platform/xamarin-forms/user-interface/xaml-basics/databindingsto_mvvm/

    Forms supports assigning a view the type of its viewmodel, so the viewmodel is created automatically:

    <ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="WidgetProjectNamespace"
             x:Class="WidgetProjectNamespace.MasterPage"
             Title="Master Page">
        <ContentPage.BindingContext>
          <local:MasterViewModel />
        </ContentPage.BindingContext>
        <StackLayout>
            <!-- A list of widgets goes here -->
        </StackLayout>
    </ContentPage>
    

    Let's say I have a master page with a simple list of items. The MasterViewModel will know how to retrieve the list of items and display them:

    public class MasterViewModel
    {
        ...
        public void Load()
        {
            var widgets= widgetRepository.Fetch();
            WidgetListItemSource = widgets;
        }
    }
    

    Tapping on an item will navigate to a detail page for that item. When I handle the click event, I need to navigate to a detail page by pushing it onto the stack:

    Item.OnTapped += (s, e) => Navigation.PushAsync(new DetailPage());
    

    That detail page can also have its viewmodel created automatically:

    <ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="WidgetProjectNamespace"
             x:Class="WidgetProjectNamespace.DetailPage"
             Title="Detail Page">
        <ContentPage.BindingContext>
          <local:DetailViewModel />
        </ContentPage.BindingContext>
        <!-- Some content describing the widget's details goes here -->
    </ContentPage>
    

    But how will the DetailViewModel know what model it needs to display?

    The only way to do this seems to be to tell the page directly:

    Item.OnTapped += (s, e) => Navigation.PushAsync(new DetailPage(itemModel));
    

    But this seems backwards. The viewmodel should tell the page what to display, not the other way around.

    Am I missing something? Is there a way in vanilla Xamarin.Forms to navigate to a detail page and respect MVVM principles?

    Thursday, October 8, 2015 4:49 PM

All replies

  • User89459 posted

    A ViewModel could potentially be used for more then one page. I don't think passing a VM into a page constructor is backwards.

    Thursday, October 8, 2015 5:12 PM
  • User158279 posted

    I would agree that performing the navigation in the code behind of the view isn't exactly a great option. In my current app I have navigation setup to allow the VM's to dictate the Navigation.

    I have an interface on each View

            public interface IViewPage
            {
                void Open(INavigation navigation);
                void Close(INavigation navigation);
            }
    

    I have a base class for each page that dictates a basic navigation model but then allows each page to override that if they want.

        public  class ViewPage:ContentPage,IViewPage
        {
            public ViewPage(ViewModelBase viewModel):base()
            {
                this.BindingContext =  viewModel;
                ((ViewModelBase)this.BindingContext).OnCloseView += (s, e) => {Close(SimpleIoc.Default.GetInstance<INavigation>());
                };
                ((ViewModelBase)this.BindingContext).OnOpenView += (s, e) => {Open(SimpleIoc.Default.GetInstance<INavigation>());
                }; 
            }
    
            public virtual void Open(INavigation navigation)
            {
                navigation.PushAsync(this);
            }
            public virtual void Close(INavigation navigation)
            {
                if(Navigation.NavigationStack.Count>1)
                    navigation.PopAsync();
            }
        }
    

    I register Each view with an IOC container atm Im using MVVMLite's

            public class App : Application
            {
                public App ()
                {
                    RegisterPages ();
                    PriceIndex.VM.App.Initialize (this);
                }
    
                public void RegisterPages()
                {
    
                    SimpleIoc.Default.Register<ContentPage> (() => new PreferencesPage (new PreferencesViewModel()), "PreferencesPage", false);
                    SimpleIoc.Default.Register<ContentPage> (() => new PriceListVPage (new PriceListViewModel()), "PriceListVPage", false);
                    SimpleIoc.Default.Register<ContentPage> (() => new CreditsPage (new CreditsViewModel()), "CreditsPage", false);
    
                }
    
            }
    

    I have the Following in my View Model Base

            public abstract class ViewModelBase:GalaSoft.MvvmLight.ViewModelBase
            {
                public event EventHandler OnCloseView;
                public event EventHandler<string> OnOpenView;
                public void RequestCloseView()
                {
                    if (OnCloseView != null) {
                        OnCloseView (this, new EventArgs());
                    }
                }
    
                public void RequestOpenView()
                {
                    RequestOpenView (string.Empty);
                }
                public void RequestOpenView(string namedInstance)
                {
                    if (OnOpenView != null) {
                        OnOpenView (this, namedInstance);
                    }
                }
            }
    

    What this then allows me to do is call RequestClose or RequestOpen from my ViewModel which then triggers an event that makes my views navigate. The Method of navigation is defined by my Views. How the navigation occurs whether or not its animated etc. Is to me a UI issue and thus defined by the view. But when to navigate is a logical decision I see as belonging in the VM. Thus the VM says Navigate and the Views Decide how to do it. Not sure if that helps or is just information overload :D.

    Thursday, October 8, 2015 5:39 PM
  • User2773 posted

    If you want some simple MVVM helper designed with Xamarin.Forms in mind, you could try this: https://github.com/daniel-luberda/DLToolkit.PageFactory

    You can send messages to ViewModels, Pages and a lot more. I tried to make it as simple as possible (all is handled by the lib, caching, viewmodel instantiation, etc). It doesn't collide with "standard" Xamarin.Forms workflow, you can still use its Navigation, ICommand, custom Page renderers, etc). There are also no requirements for ViewModels (they must implement INotifyPropertyChanged at minimum or IBaseMessagable for ViewModel messaging and you can use static PF.Factory instead of PageFactory property which is provided in BaseViewModel class)

    PageFactory has also ReplaceViewModel method:

    PageFactory.GetMessagablePageFromCache<XamlSecondViewModel>()
        .ReplaceViewModel(yourNewViewModelInstanceHere)
            .SendMessageToViewModel(message: "Hello", sender: this, arg: Guid.NewGuid())
            .PushPage();
    

    It's just as simple as (whole working XAML PageFactory app here):

    App.cs:

    public class App : Application
    {
        public App()
        {
            MainPage = new XamarinFormsPageFactory().Init<XamlFirstViewModel, PFNavigationPage>();
        }   
    }
    

    XamlFirstPage.cs:

    public partial class XamlFirstPage : PFContentPage<XamlFirstViewModel>
    {
        public XamlFirstPage()
        {
            InitializeComponent();
        }
    }
    

    XamlFirstPage.xaml:

    <?xml version="1.0" encoding="UTF-8"?>
    <local:PFContentPage 
        xmlns="http://xamarin.com/schemas/2014/forms" 
        xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
        x:Class="PageFactoryTest.Pages.XamlFirstPage"
        xmlns:local="clr-namespace:DLToolkit.PageFactory"
        x:TypeArguments="PageFactoryTest.ViewModels.XamlFirstViewModel">
        <ContentPage.Content>
            <Button Text="Open Second Page (and send message)" Command="{Binding OpenSecondPageCommand}"/>
        </ContentPage.Content>
    </local:PFContentPage>
    

    XamlFirstViewModel.cs:

    public class XamlFirstViewModel : BaseViewModel
    {
        public XamlFirstViewModel()
        {
            OpenSecondPageCommand = new PageFactoryCommand(() => 
                PageFactory.GetMessagablePageFromCache<XamlSecondViewModel>()
                    .SendMessageToViewModel("Hello", this, Guid.NewGuid())
                    .PushPage());
        }
    
        public IPageFactoryCommand OpenSecondPageCommand { get; private set; }
    }
    

    XamlSecondPage.cs:

    public partial class XamlSecondPage : PFContentPage<XamlSecondViewModel>
    {
        public XamlSecondPage()
        {
            InitializeComponent();
        }
    }
    

    XamlSecondPage.xaml:

    <?xml version="1.0" encoding="UTF-8"?>
    <local:PFContentPage 
        xmlns="http://xamarin.com/schemas/2014/forms" 
        xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
        x:Class="PageFactoryTest.Pages.XamlSecondPage"
        xmlns:local="clr-namespace:DLToolkit.PageFactory"
        x:TypeArguments="PageFactoryTest.ViewModels.XamlSecondViewModel">
        <ContentPage.Content>
            <Label Text="{Binding ReceivedMessage}" VerticalOptions="CenterAndExpand"/>
        </ContentPage.Content>
    </local:PFContentPage>
    

    XamlSecondViewModel.cs:

    public class XamlSecondViewModel : BaseViewModel
    {
        public override void PageFactoryMessageReceived(string message, object sender, object arg)
        {
            ReceivedMessage = string.Format(
                "Received message: {0} with arg: {1} from: {2}",
                message, arg, sender.GetType());
        }
    
        public string ReceivedMessage {
            get { return GetField<string>(); }
            set { SetField(value); }
        }
    }
    
    Thursday, October 8, 2015 10:33 PM
  • User159321 posted

    @DavidStrickland0 That is helpful, in that it seems the answer is "no, Forms really isn't designed for that, use a real MVVM framework."

    We're going with FreshMVVM for now: https://forums.xamarin.com/discussion/40655/freshmvvm-a-mvvm-framework-for-xamarin-forms It's not perfect, but all it does is basically fill in the gaps in stock Xamarin (viewmodel-based navigation, constructor injection) using TinyMVVM, wich is exactly what we were looking for.

    Monday, October 26, 2015 10:23 PM