Parece que este tópico é muito popular e será triste não mencionar aqui que existe uma forma alternativa - ViewModel First Navigation
. A maioria dos frameworks MVVM que o utilizam, no entanto, se você quiser entender do que se trata, continue lendo.
Toda a documentação oficial do Xamarin.Forms está demonstrando uma solução simples, mas um pouco diferente do MVVM puro. Isso ocorre porque o Page
(View) não deve saber nada sobre o ViewModel
e vice-versa. Aqui está um ótimo exemplo dessa violação:
// 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>
Se você tem um aplicativo de 2 páginas, essa abordagem pode ser boa para você. No entanto, se você estiver trabalhando em uma solução para uma grande empresa, é melhor escolher uma ViewModel First Navigation
abordagem. É uma abordagem um pouco mais complicada, mas muito mais limpa, que permite navegar entre em ViewModels
vez de navegar entre Pages
(visualizações). Uma das vantagens ao lado da separação clara de interesses é que você pode facilmente passar parâmetros para o próximoViewModel
ou executar um código de inicialização assíncrono logo após a navegação. Agora, para detalhes.
(Vou tentar simplificar todos os exemplos de código tanto quanto possível).
1. Em primeiro lugar, precisamos de um local onde possamos registrar todos os nossos objetos e, opcionalmente, definir seu tempo de vida. Para este assunto, podemos usar um contêiner IOC, você pode escolher um. Neste exemplo vou usar o Autofac (é um dos mais rápidos disponíveis). Podemos manter uma referência a ele no App
para que esteja disponível globalmente (não é uma boa ideia, mas necessária para simplificação):
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. Precisaremos de um objeto responsável por recuperar uma Page
(Visualização) para um específico ViewModel
e vice-versa. O segundo caso pode ser útil no caso de configuração da página raiz / principal do aplicativo. Para isso, devemos concordar com uma convenção simples de que todos os ViewModels
devem estar no ViewModels
diretório e Pages
(Visualizações) devem estar no Views
diretório. Em outras palavras, ViewModels
deve residir no [MyApp].ViewModels
namespace e Pages
(Views) no [MyApp].Views
namespace. Além disso, devemos concordar que WelcomeView
(Página) deve ter um WelcomeViewModel
e etc. Aqui está um exemplo de código de um 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 o caso de configuração de uma página raiz, precisaremos ViewModelLocator
definir o BindingContext
automaticamente:
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>
4. Por fim, precisaremos de uma abordagem NavigationService
que apoie 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 você pode ver, existe uma BaseViewModel
- classe base abstrata para todas as ViewModels
onde você pode definir métodos como os InitializeAsync
que serão executados logo após a navegação. E aqui está um exemplo de navegação:
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 você entende, essa abordagem é mais complicada, mais difícil de depurar e pode ser confusa. No entanto, existem muitas vantagens, além disso, você não precisa implementá-lo sozinho, já que a maioria dos frameworks MVVM oferece suporte para ele fora da caixa. O exemplo de código demonstrado aqui está disponível no github .
Existem muitos artigos bons sobre ViewModel First Navigation
abordagem e um eBook grátis Enterprise Application Patterns usando Xamarin.Forms que explica este e muitos outros tópicos interessantes em detalhes.