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 ViewModel
y 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 Navigation
enfoque. Es un enfoque un poco más complicado pero mucho más limpio que le permite navegar entre en ViewModels
lugar 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 App
para 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 ViewModel
y 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 ViewModels
deberían estar en el ViewModels
directorio y Pages
(Vistas) deberían estar en el Views
directorio. En otras palabras, ViewModels
debería vivir en el [MyApp].ViewModels
espacio de nombres y Pages
(Vistas) en el [MyApp].Views
espacio de nombres. Además de eso, deberíamos estar de acuerdo en que WelcomeView
(Página) debería tener un WelcomeViewModel
y 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 ViewModelLocator
que establecerá BindingContext
automá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 NavigationService
que 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 BaseViewModel
clase base abstracta para todos los ViewModels
métodos donde puede definir métodos InitializeAsync
que 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 Navigation
enfoque 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.