Навигация и DI

Я пытаюсь создать стандартный код для использования в моих приложениях xamarin.forms. Я хочу иметь способ перемещаться между моделями просмотра и вей, чтобы правильно реализовать внедрение зависимостей. Что я сейчас делаю для навигации:

await Navigation.PushAsync(new SecondPageView());

А для DI:

 var test = DependencyService.Get<ITestService>();
 WelcomeMessage = test.GetSystemWelcome();

Я знаю, что правильным способом реализации Di является создание интерфейса и продолжение этого шага, но проблема в том, что, когда я пытаюсь, мне не удается создать хорошую систему навигации (например, зарегистрировать представление и модель представления в отдельном файле) .

У кого-нибудь есть образец, который я могу посмотреть? Или, может быть, какие-то указания для продолжения?

PD: Я стараюсь избегать таких фреймворков, как MvvMcross.

Заранее спасибо!

Я не мог понять, чего именно ты хочешь. Вы хотите реализовать свой собственный ViewModelLocator, как это делает Prism?

Diego Rafael Souza 18.05.2018 22:23

@DiegoRafaelSouza Да, я хочу знать, как лучше всего реализовать DI и структуру навигации, такую ​​как MvvmCross (например), но без ее использования (если это возможно, конечно)

flaurens 18.05.2018 22:30

Взгляните на эти два сообщения mallibone.com/post/xamarin.forms-navigation-with-mvvm-lightalexdunn.org/2017/06/01/…. Я понимаю, что вы не хотите использовать фреймворк, но MVVM Light настолько прост и мал, что вы можете взглянуть на него

Johannes 18.05.2018 22:36

@Johannes спасибо за ответ, проверю обе ссылки

flaurens 18.05.2018 22:51

Это вполне возможно, но я не думаю, что это того стоит. Эти фреймворки слишком легкие и уже решают множество проблем, которые доставят вам головную боль, как только вы начнете разрабатывать и / или использовать.

Diego Rafael Souza 18.05.2018 22:53
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
5
1 125
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

(Я постараюсь максимально упростить все примеры кода).

1. Прежде всего нам нужно место, где мы могли бы зарегистрировать все наши объекты и, при желании, определить их время жизни. Для этого мы можем использовать контейнер IOC, вы можете выбрать его сами. В этом примере я буду использовать Автофак (это один из самых быстрых доступных). Мы можем сохранить ссылку на него в App, чтобы он был доступен глобально (не очень хорошая идея, но необходима для упрощения):

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. Нам понадобится объект, отвечающий за получение Page (View) для конкретного ViewModel и наоборот. Второй случай может быть полезен в случае настройки корневой / главной страницы приложения. Для этого мы должны договориться о простом соглашении, что все ViewModels должны находиться в каталоге ViewModels, а Pages (представления) должны находиться в каталоге Views. Другими словами, ViewModels должен находиться в пространстве имен [MyApp].ViewModels, а Pages (представления) - в пространстве имен [MyApp].Views. В дополнение к этому мы должны согласиться с тем, что WelcomeView (Page) должен иметь WelcomeViewModel и т. д. Вот пример кода маппера:

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. В случае установки корневой страницы нам понадобится своего рода ViewModelLocator, который автоматически установит BindingContext:

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.Наконец, нам понадобится NavigationService, который будет поддерживать подход 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);
    }
}

Как вы можете видеть, существует BaseViewModel - абстрактный базовый класс для всех ViewModels, в котором вы можете определить такие методы, как InitializeAsync, которые будут выполняться сразу после навигации. А вот пример навигации:

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>());
    }
}

Как вы понимаете, этот подход более сложен, труднее отлаживать и может сбивать с толку. Однако есть много преимуществ, плюс вам на самом деле не нужно реализовывать его самостоятельно, поскольку большинство фреймворков MVVM поддерживают его из коробки. Пример кода, который здесь демонстрируется, доступен на github.

Есть много хороших статей о подходе ViewModel First Navigation и есть бесплатная электронная книга Шаблоны корпоративных приложений с использованием Xamarin.Forms, в которой подробно объясняется эта и многие другие интересные темы.

Отличная работа! Приветствую вас за этот отличный ответ, поэтому я люблю сообщество :)

Johannes 18.05.2018 22:53

Большое спасибо! Я уже проверял это в электронной книге Xamarin.Forms, но теряюсь, когда пытаюсь реализовать в своем коде. В любом случае большое спасибо за вашу помощь.

flaurens 18.05.2018 22:54

Другие вопросы по теме