Parece que este hilo es muy popular y será triste no mencionar aquí que hay una forma alternativa - ViewModel First Navigation. La mayoría de los frameworks MVVM lo usan, sin embargo, si quieres entender de qué se trata, continúa leyendo.
Toda la documentación oficial de Xamarin.Forms está demostrando una solución simple, aunque un poco no MVVM pura. Esto se debe a que Page(View) no debería saber nada sobre el ViewModely viceversa. Aquí hay un gran ejemplo de esta violación:
// C# version
public partial class MyPage : ContentPage
{
public MyPage()
{
InitializeComponent();
// Violation
this.BindingContext = new MyViewModel();
}
}
// XAML version
<?xml version="1.0" encoding="utf-8"?>
<ContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodels="clr-namespace:MyApp.ViewModel"
x:Class="MyApp.Views.MyPage">
<ContentPage.BindingContext>
<!-- Violation -->
<viewmodels:MyViewModel />
</ContentPage.BindingContext>
</ContentPage>
Si tiene una aplicación de 2 páginas, este enfoque puede ser bueno para usted. Sin embargo, si está trabajando en una gran solución empresarial, será mejor que adopte un ViewModel First Navigationenfoque. Es un enfoque un poco más complicado pero mucho más limpio que le permite navegar entre en ViewModelslugar de navegar entre Pages(Vistas). Una de las ventajas, además de una clara separación de preocupaciones, es que puede pasar fácilmente parámetros al siguienteViewModel o ejecutar un código de inicialización asincrónico justo después de la navegación. Ahora a los detalles.
(Intentaré simplificar todos los ejemplos de código tanto como sea posible).
1. En primer lugar, necesitamos un lugar donde podamos registrar todos nuestros objetos y, opcionalmente, definir su vida útil. Para este asunto podemos usar un contenedor IOC, puede elegir uno usted mismo. En este ejemplo usaré Autofac (es uno de los más rápidos disponibles). Podemos mantener una referencia a él Apppara que esté disponible a nivel mundial (no es una buena idea, pero es necesaria para simplificar):
public class DependencyResolver
{
static IContainer container;
public DependencyResolver(params Module[] modules)
{
var builder = new ContainerBuilder();
if (modules != null)
foreach (var module in modules)
builder.RegisterModule(module);
container = builder.Build();
}
public T Resolve<T>() => container.Resolve<T>();
public object Resolve(Type type) => container.Resolve(type);
}
public partial class App : Application
{
public DependencyResolver DependencyResolver { get; }
// Pass here platform specific dependencies
public App(Module platformIocModule)
{
InitializeComponent();
DependencyResolver = new DependencyResolver(platformIocModule, new IocModule());
MainPage = new WelcomeView();
}
/* The rest of the code ... */
}
2. Necesitaremos un objeto responsable de recuperar una Page(Vista) para una específica ViewModely viceversa. El segundo caso puede ser útil en caso de configurar la página raíz / principal de la aplicación. Para eso, deberíamos acordar una convención simple de que todos ViewModelsdeberían estar en el ViewModelsdirectorio y Pages(Vistas) deberían estar en el Viewsdirectorio. En otras palabras, ViewModelsdebería vivir en el [MyApp].ViewModelsespacio de nombres y Pages(Vistas) en el [MyApp].Viewsespacio de nombres. Además de eso, deberíamos estar de acuerdo en que WelcomeView(Página) debería tener un WelcomeViewModely etc. Aquí hay un ejemplo de código de un mapeador:
public class TypeMapperService
{
public Type MapViewModelToView(Type viewModelType)
{
var viewName = viewModelType.FullName.Replace("Model", string.Empty);
var viewAssemblyName = GetTypeAssemblyName(viewModelType);
var viewTypeName = GenerateTypeName("{0}, {1}", viewName, viewAssemblyName);
return Type.GetType(viewTypeName);
}
public Type MapViewToViewModel(Type viewType)
{
var viewModelName = viewType.FullName.Replace(".Views.", ".ViewModels.");
var viewModelAssemblyName = GetTypeAssemblyName(viewType);
var viewTypeModelName = GenerateTypeName("{0}Model, {1}", viewModelName, viewModelAssemblyName);
return Type.GetType(viewTypeModelName);
}
string GetTypeAssemblyName(Type type) => type.GetTypeInfo().Assembly.FullName;
string GenerateTypeName(string format, string typeName, string assemblyName) =>
string.Format(CultureInfo.InvariantCulture, format, typeName, assemblyName);
}
3.Para el caso de configurar una página raíz, necesitaremos algo ViewModelLocatorque establecerá BindingContextautomáticamente:
public static class ViewModelLocator
{
public static readonly BindableProperty AutoWireViewModelProperty =
BindableProperty.CreateAttached("AutoWireViewModel", typeof(bool), typeof(ViewModelLocator), default(bool), propertyChanged: OnAutoWireViewModelChanged);
public static bool GetAutoWireViewModel(BindableObject bindable) =>
(bool)bindable.GetValue(AutoWireViewModelProperty);
public static void SetAutoWireViewModel(BindableObject bindable, bool value) =>
bindable.SetValue(AutoWireViewModelProperty, value);
static ITypeMapperService mapper = (Application.Current as App).DependencyResolver.Resolve<ITypeMapperService>();
static void OnAutoWireViewModelChanged(BindableObject bindable, object oldValue, object newValue)
{
var view = bindable as Element;
var viewType = view.GetType();
var viewModelType = mapper.MapViewToViewModel(viewType);
var viewModel = (Application.Current as App).DependencyResolver.Resolve(viewModelType);
view.BindingContext = viewModel;
}
}
// Usage example
<?xml version="1.0" encoding="utf-8"?>
<ContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodels="clr-namespace:MyApp.ViewModel"
viewmodels:ViewModelLocator.AutoWireViewModel="true"
x:Class="MyApp.Views.MyPage">
</ContentPage>
Finalmente, necesitaremos un enfoque NavigationServiceque apoye ViewModel First Navigation:
public class NavigationService
{
TypeMapperService mapperService { get; }
public NavigationService(TypeMapperService mapperService)
{
this.mapperService = mapperService;
}
protected Page CreatePage(Type viewModelType)
{
Type pageType = mapperService.MapViewModelToView(viewModelType);
if (pageType == null)
{
throw new Exception($"Cannot locate page type for {viewModelType}");
}
return Activator.CreateInstance(pageType) as Page;
}
protected Page GetCurrentPage()
{
var mainPage = Application.Current.MainPage;
if (mainPage is MasterDetailPage)
{
return ((MasterDetailPage)mainPage).Detail;
}
// TabbedPage : MultiPage<Page>
// CarouselPage : MultiPage<ContentPage>
if (mainPage is TabbedPage || mainPage is CarouselPage)
{
return ((MultiPage<Page>)mainPage).CurrentPage;
}
return mainPage;
}
public Task PushAsync(Page page, bool animated = true)
{
var navigationPage = Application.Current.MainPage as NavigationPage;
return navigationPage.PushAsync(page, animated);
}
public Task PopAsync(bool animated = true)
{
var mainPage = Application.Current.MainPage as NavigationPage;
return mainPage.Navigation.PopAsync(animated);
}
public Task PushModalAsync<TViewModel>(object parameter = null, bool animated = true) where TViewModel : BaseViewModel =>
InternalPushModalAsync(typeof(TViewModel), animated, parameter);
public Task PopModalAsync(bool animated = true)
{
var mainPage = GetCurrentPage();
if (mainPage != null)
return mainPage.Navigation.PopModalAsync(animated);
throw new Exception("Current page is null.");
}
async Task InternalPushModalAsync(Type viewModelType, bool animated, object parameter)
{
var page = CreatePage(viewModelType);
var currentNavigationPage = GetCurrentPage();
if (currentNavigationPage != null)
{
await currentNavigationPage.Navigation.PushModalAsync(page, animated);
}
else
{
throw new Exception("Current page is null.");
}
await (page.BindingContext as BaseViewModel).InitializeAsync(parameter);
}
}
Como puede ver, hay una BaseViewModelclase base abstracta para todos los ViewModelsmétodos donde puede definir métodos InitializeAsyncque se ejecutarán inmediatamente después de la navegación. Y aquí hay un ejemplo de navegación:
public class WelcomeViewModel : BaseViewModel
{
public ICommand NewGameCmd { get; }
public ICommand TopScoreCmd { get; }
public ICommand AboutCmd { get; }
public WelcomeViewModel(INavigationService navigation) : base(navigation)
{
NewGameCmd = new Command(async () => await Navigation.PushModalAsync<GameViewModel>());
TopScoreCmd = new Command(async () => await navigation.PushModalAsync<TopScoreViewModel>());
AboutCmd = new Command(async () => await navigation.PushModalAsync<AboutViewModel>());
}
}
Como comprenderá, este enfoque es más complicado, más difícil de depurar y puede resultar confuso. Sin embargo, hay muchas ventajas, además de que en realidad no tiene que implementarlo usted mismo, ya que la mayoría de los marcos MVVM lo admiten desde el primer momento. El ejemplo de código que se muestra aquí está disponible en github .
Hay muchos buenos artículos sobre el ViewModel First Navigationenfoque y hay un libro electrónico gratuito de patrones de aplicación empresarial que usa Xamarin.Forms eBook que explica este y muchos otros temas interesantes en detalle.