Антипаттерн MVVM Framework — технические службы Marcus

Посмотрим правде в глаза: программисты любят фреймворки. Фреймворк — это набор предварительно упакованного кода, который поддерживает определенную функциональность. Чем более фундаментальной является функциональность, тем более важным кажется фреймворк. Когда все сообщество программистов примет фреймворк, это закрепит сделку. Именно это вывело MVVM Frameworks на передний план разработки Xamarin.Forms. Никто не удосужился проанализировать эти фреймворки на предмет их соответствия философии дизайна C# SOLID.

«Противоположность смелости… не трусость, а конформизм».

Ролло Мэй

Давайте держать вещи НАДЕЖНЫМИ

Принципы SOLID требуют, чтобы мы создавали программы, которые свободно связаны друг с другом и открыты для изменений, пока взаимодействующие элементы подчиняются своим интерфейсам. Это первое, что выходит из окна с фреймворками MVVM.

Вот пример представления, отображающего данные на основе контракта интерфейса:

<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns=" xmlns:x=" x:Class="MvvmAntipattern.StateMachine.Views.Pages.AnimalPage" > <StackLayout VerticalOptions="Start" HorizontalOptions="FillAndExpand" > <Label Text="I Am A:" /> <Label Text="{Binding WhatAmI,Mode=OneWay}" /> <Label Text="I Like To Eat:" Margin="0,10,0,0" /> <Label Text="{Binding LikeToEat,Mode=OneWay}" /> <Label Text="I Am Big" Margin="0,10,0,0" /> <Switch IsToggled="{Binding IAmBig,Mode=TwoWay}" /> <Label Text="I Look Like This:" Margin="0,10,0,0" /> <Image Source="{Binding MyImageSource,Mode=OneWay}" WidthRequest="150" HeightRequest="150" /> <Button Text="Move" Command="{Binding MoveCommand,Mode=OneWay}" Margin="0,10,0,0" /> <Button Text="MakeNoise" Command="{Binding MakeNoiseCommand,Mode=OneWay}" Margin="0,10,0,0" /> </StackLayout> </ContentPage>

BindingContext может быть установлен для любого класса, реализующего этот интерфейс:

public interface IAnimal : IMakeBigDecisions { string WhatAmI { get; } string LikeToEat { get; } string MyImageSource { get; } Command MakeNoiseCommand { get; } Command MoveCommand { get; } }

Обратите внимание на вспомогательный интерфейс:

public interface IMakeBigDecisions { bool IAmBig { get; set; } }

Что еще более важно, BindingContext одновременно изменчив и непредсказуем. Именно это мы подразумеваем под полиморфным или «открытым». Предположим, что приложение получает событие изменения состояния. Это событие требует, чтобы BindingContext изменился на ICat. Или IBird. Или IDog.

public interface ICat : IAnimal { } public interface IBird: IAnimal { } public interface IDog: IAnimal { }

Вот физические файлы, созданные инфраструктурой MVVM в этом сценарии:

  • AnimalPage.cs или же AnimalView.cs – представление, которое взаимодействует с пользователем.
  • AnimalViewModel.cs — Реализует IAnimal.csно не знает о ICat, IBirdили же iDog.
  • AnimalModel.cs – обеспечивает резервные данные для AnimalViewModel. Это также ограничено, поскольку он может предоставить животное только для модели представления животных, которая затем превращает его в представление животных.

Версия приложения MVVM Framework не может ничего сделать, кроме как обобщить всех животных в одно животное.

Просмотр модели для просмотра модели Навигация — это Фантазия

В средние века возникла легенда о Святом Граале. Рыцари со всего мира годами преследовали его. Но так и не нашли. Это должно заставить нас задуматься, когда мы слышим о фреймворках MVVM и их святом Граале: просмотр модели для просмотра навигации по модели.

Большинство приложений следуют этому заезженному (игра слов) Шаблон проектирования MVVM:

<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns=" xmlns:x=" x:Class="MvvmAntipattern.TiredAndTrue.TiredAndTrueMainPage"> <ContentPage.Content> <Button Text="Click Me!" Command="{Binding ClickMeCommand,Mode=OneWay}" VerticalOptions="CenterAndExpand" HorizontalOptions="CenterAndExpand" /> </ContentPage.Content> </ContentPage>
public class TiredAndTrueMainViewModel { public TiredAndTrueMainViewModel() { ClickMeCommand = newCommand(() => { var nextPage = newTiredAndTrueSecondPage() { BindingContext = newTiredAndTrueSecondViewModel() }; Application.Current.MainPage = nextPage; }); } public ICommand ClickMeCommand { get; set; } }

То же самое относится к основным сценариям деталей с меню, где кнопка находится в _ меню _ а не на _ страница _.

Проблема в том, что страница (или меню) определяет следующее представление и модель представления. Этот подход _ тесно связанный _. Страница недостаточно всезнающая, чтобы точно знать, что делать в том или ином сценарии.

Нетрудно понять, как возникла идея навигации по модели представления. В MVVM мы воспринимаем большинство событий и привязок в модели представления. Но MVVM также требует, чтобы мы сохранили модель представления _ разделенный _ из вида. Что еще осталось? _ следующий _ просмотреть модель.

Все фантазии одинаковы: мы хотим чего-то, чего не можем иметь. Мы готовы сделать все, чтобы получить его. Это приводит к прыжкам веры и необдуманным решениям.

Трюки — это весело; На этом веселье заканчивается

Фреймворки MVVM совершают скачок от модели представления к другой модели представления (и подразумеваемый вид) через простой _ соглашение об именах файлов _. Десять лет назад это было бы смехотворно, когда мы отказались от Visual Basic, потому что он был таким неряшливым и неряшливым. Такого рода логика имеет идеальный «смысл».

В последнем примере инфраструктура MVVM требует от нас создания следующих файлов:

  • TiredAndTrueSecondViewModel.cs
  • TiredAndTrueViewModel.cs
  • TiredAndTrueSecondPage.xaml
  • TiredAndTrueSecondViewModel.cs

Для перехода с главной страницы на вторую используется единственная реальная информация: _ текстовый префикс _ имени файла:

  • TiredAndTrueMain
  • TiredAndTrueSecond

Каркас _ отражается на источнике _ к _ автоматически создавать экземпляры _ модель представления и ее целевое представление.

Это _ нет _ форма навигации. _ Это трюк с отражением _. Навигация не является существенной или ограничивается моделью представления. Это иллюзия. Можно открыть любую модель представления и представление из любого места в приложении и в любое время. Так что это не точно описано или реализовано. Это на самом деле _ строка для просмотра навигации по модели _.

Activator.CreateInstance вам не друг

Фреймворк MVVM использует Activator.CreateInstance и аналогичные инструменты системного уровня для управления его экземплярами. Эти методы опасны в мобильном приложении, потому что они делают эти созданные классы «невидимыми» для компилятора. Компоновщик времени компиляции игнорирует любой класс, который не связан напрямую с исходным кодом через обычные программные отношения. Этот «трюк» слишком умен для компилятора. Хак, который лечит ограничение (атрибут СОХРАНИТЬ) неуклюже и утомительно.

Самая большая проблема с фреймворками MVVM вовсе не техническая, а чрезвычайное нарушение принципов SOLID. Этот подход требует однозначного согласования между:

  • Вид
  • Модель представления
  • Модель

Это _ смехотворный _. SOLID говорит, что представление может анимировать любое количество моделей представлений, которые могут изменяться во время выполнения, и что данные для моделей представлений также могут вводиться «на лету». Фреймворки MVVM требуют, чтобы программисты создавали «игрушечные» приложения с упрощенной структурой и нулевой гибкостью дизайна.

Представляем конечный автомат

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

Навигационная система:

  • Это _ общесистемная служба _ доступный в любое время любым лицом с разрешением на изменение текущей страницы. (Чтобы добавить сложности и нюансов, мы можем создать несколько навигационных сервисов, но это выходит за рамки данного анализа..)
  • Делает _ нет _ требуют какого-либо соглашения об именах файлов.
  • Обладает _ продуманная бизнес-логика _ чтобы определить, как сопоставлять представления с моделями представлений и, возможно, даже модели (данных) с моделями представлений.
  • Определяет следующая страница (просмотр) изначальноа затем модель представления и данные после этого.
  • _ Создание экземпляров элементов управления _ для поощрения полиморфизма. Мы можем передать любой параметр любому конструктору на основе правил времени выполнения. Кроме того, это позволяет избежать проблем с компоновщиком, связанных с Activator.CreateInstance.
  • _ Сохраняет свое текущее состояние _ для помощи в принятии решений.
  • По возможности избегает навигации из вида. Большинство изменений навигации должно быть результатом _ События _ или же _ команды _. Они будут часто (но не всегда) происходят внутри моделей представлений. Но это делает нет сделайте эту модель представления для просмотра системы навигации по модели, так как модель представления не является назначения навигационного процесса. Модель представления, скорее всего, источник этого процесса.
  • Инкапсулирует свою логику конфиденциально и без ненужного взаимодействия с другими классами.

Государственная машина отвечает этим требованиям.

Интерфейс:
public interface IStateMachineBase : IDisposable { // A way of knowing the current app state, though this should not be commonly referenced. string CurrentAppState { get; } // The normal way of changing states void GoToAppState<T>(string newState, T payload = default(T), bool preventStackPush = false); // Sets the startup state for the app on initial start (or restart). void GoToStartUpState(); // Goes to the default landing page; for convenience only void GoToLandingPage(bool preventStackPush = true); // Access to the forms messenger; also for convenience. IForms MessengerMessenger { get; set; } }
Базовый класс:
/// <summary> /// A controller to manage which views and view models are shown for a given state /// </summary> public abstract class StateMachineBase : IStateMachineBase { private Page _lastPage; private string _lastAppState; public abstract string AppStartUpState { get; } public void GoToAppState<T>(string newState, T payload = default(T), bool preventStackPush = false) { if (_lastAppState.IsSameAs(newState)) { return; } // Raise an event to notify the nav bar that the back-stack requires modification. // Send in the last app state, *not* the new one. FormsMessengerUtils.Send(new AppStateChangedMessage(_lastAppState, preventStackPush)); // CurrentAppState = newState; _lastAppState = newState; // Not awaiting here because we do not directly change the Application.Current.MainPage. That is done through a message. RespondToAppStateChange(newState, payload, preventStackPush); } // public string CurrentAppState { get; private set; } public abstract IMenuNavigationState[] MenuItems { get; } // Sets the startup state for the app on initial start (or restart). public void GoToStartUpState() { FormsMessengerUtils.Send(new AppStartUpMessage()); GoToAppState<NoPayload>(AppStartUpState, null, true); } public abstract void GoToLandingPage(bool preventStackPush = true); public void Dispose() { ReleaseUnmanagedResources(); GC.SuppressFinalize(this); } protected abstract void RespondToAppStateChange<PayloadT>(string newState, PayloadT payload, bool preventStackPush); protected void CheckAgainstLastPage(Type pageType, Func<Page> pageCreator, Func<IViewModelBase> viewModelCreator, bool preventStackPush) { // If the same page, keep it if (_lastPage != null && _lastPage.GetType() == pageType) { FormsMessengerUtils.Send(new BindingContextChangeRequestMessage { Payload = viewModelCreator(), PreventNavStackPush = preventStackPush }); return; } // ELSE create both the page and view model var page = pageCreator(); page.BindingContext = viewModelCreator(); FormsMessengerUtils.Send(new MainPageChangeRequestMessage { Payload = page, PreventNavStackPush = preventStackPush }); _lastPage = page; } protected virtual void ReleaseUnmanagedResources() { } ~StateMachineBase() { ReleaseUnmanagedResources(); }

Этот класс является абстрактным и публикуется в разделяемой библиотеке. Для любого данного приложения мы получаем и переопределяем RespondToAppStateChange реализовать логику:

public class FormsStateMachine : StateMachineBase { public const string NO_APP_STATE = "None"; public const string AUTO_SIGN_IN_APP_STATE = "AttemptAutoSignIn"; public const string ABOUT_APP_STATE = "About"; public const string PREFERENCES_APP_STATE = "Preferences"; public const string NO_ANIMAL_APP_STATE = "No Animal"; public const string CAT_ANIMAL_APP_STATE = "Cat"; public const string BIRD_ANIMAL_APP_STATE = "Bird"; public const string DOG_ANIMAL_APP_STATE = "Dog"; public static readonly string[] APP_STATES = { NO_APP_STATE, AUTO_SIGN_IN_APP_STATE, NO_ANIMAL_APP_STATE, CAT_ANIMAL_APP_STATE, BIRD_ANIMAL_APP_STATE, DOG_ANIMAL_APP_STATE, ABOUT_APP_STATE, PREFERENCES_APP_STATE }; public override string AppStartUpState => AUTO_SIGN_IN_APP_STATE; public override IMenuNavigationState[] MenuItems => new IMenuNavigationState[] { new MenuNavigationState(GetMenuOrderFromAppState(ABOUT_APP_STATE), ABOUT_APP_STATE, "About", ABOUT_APP_STATE), new MenuNavigationState(GetMenuOrderFromAppState(BIRD_ANIMAL_APP_STATE), BIRD_ANIMAL_APP_STATE, "ANIMALS", BIRD_ANIMAL_APP_STATE), new MenuNavigationState(GetMenuOrderFromAppState(CAT_ANIMAL_APP_STATE), CAT_ANIMAL_APP_STATE, "ANIMALS", CAT_ANIMAL_APP_STATE), new MenuNavigationState(GetMenuOrderFromAppState(DOG_ANIMAL_APP_STATE), DOG_ANIMAL_APP_STATE, "ANIMALS", DOG_ANIMAL_APP_STATE), new MenuNavigationState(GetMenuOrderFromAppState(NO_ANIMAL_APP_STATE), NO_ANIMAL_APP_STATE, "ANIMALS", NO_ANIMAL_APP_STATE), new MenuNavigationState(GetMenuOrderFromAppState(ABOUT_APP_STATE), PREFERENCES_APP_STATE, "Preferences", PREFERENCES_APP_STATE), }; public override void GoToLandingPage(bool preventStackPush = true) { GoToAppState<NoPayload>(NO_ANIMAL_APP_STATE, null, preventStackPush); } protected override void RespondToAppStateChange<PayloadT>(string newState, PayloadT payload, bool preventStackPush) { var titleStr = payload is IMenuNavigationState pageAsNavState ? pageAsNavState.ViewTitle : ""; switch (newState) { case ABOUT_APP_STATE: CheckAgainstLastPage(typeof(DummyPage), () => new DummyPage(), () => new AboutViewModel(this) {PageTitle = titleStr}, preventStackPush); break; case PREFERENCES_APP_STATE: CheckAgainstLastPage(typeof(DummyPage), () => new DummyPage(), () => new PreferencesViewModel(this) { PageTitle = titleStr }, preventStackPush); break; case CAT_ANIMAL_APP_STATE: CheckAgainstLastPage(typeof(AnimalStage), () => new AnimalStage(), () => new CatViewModel(this, new CatData()) { PageTitle = titleStr }, preventStackPush); break; case BIRD_ANIMAL_APP_STATE: CheckAgainstLastPage(typeof(AnimalStage), () => new AnimalStage(), () => new BirdViewModel(this, new BirdData()) { PageTitle = titleStr }, preventStackPush); break; case DOG_ANIMAL_APP_STATE: CheckAgainstLastPage(typeof(AnimalStage), () => new AnimalStage(), () => new DogViewModel(this, new DogData()) { PageTitle = titleStr }, preventStackPush); break; default: //NO_ANIMAL_APP_STATE: CheckAgainstLastPage(typeof(AnimalStage), () => new AnimalStage(), () => new NoAnimalViewModel(this, null) { PageTitle = titleStr }, true); break; } } private void AttemptAutoSignIn() { // Assuming true; also, prevent stack push so we don't go back into this state, as it is "finished" GoToAppState<NoPayload>(NO_ANIMAL_APP_STATE, null, true); }

Конечный автомат создает следующее представление и модель представления на основе _ бизнес-логика _. Здесь вводятся все параметры, что делает его на 100% совместимым с философией внедрения зависимостей.

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

private void CheckAgainstLastPage( Type pageType, Func<Page> pageCreator, Func<IViewModelBase> viewModelCreator, bool preventStackPush)

PreventStackPush здесь на тот случай, если мы перейдем на какую-то страницу, которая не поддерживает систему навигации. Страница входа — это как раз такое животное. Для этой страницы мы бы передали false, и бэк-стек проигнорировал бы эту страницу. Пользователь не может вернуться к входу в систему. (_ Примечание : Я не приводил подробный пример этой функции_.)

Навигация на основе сообщений

В приведенных выше примерах, когда мы, наконец, решаем, что делать, мы отправляем Xamarin.Forms сообщение с просьбой внести изменения. Это новый взгляд на навигацию, а также на события в целом. Я раскрою эту тему в отдельной статье. Короче говоря, приложение прослушивает это изменение в app.xaml.cs:

private void MainPageChanged(object sender, MainPageChangeRequestMessage messageArgs) { // Try to avoid changing the page is possible if ( messageArgs?.Payload == null || MainPage == null || ( _lastMainPage != null && _lastMainPage.GetType() == messageArgs.Payload.GetType() ) ) { return; } MainPage = messageArgs.Payload; _lastMainPage = MainPage; } private void BindingContextPageChanged(object sender, BindingContextChangeRequestMessage messageArgs) { if (MainPage != null) { MainPage.BindingContext = messageArgs.Payload; } }

Мы никогда не пытаемся изменить Application.Current.MainPage из любого другого места в этом приложении.

Панель навигации/заголовка

Мы начали эту статью с обсуждения негативного влияния шаблона проектирования MVVM Framework. Теперь мы немного отошли от темы навигации. Я подробно расскажу об этом в другой статье. Для ясности, вот как я подключил новый конечный автомат и навигационную систему, чтобы пользователь мог взаимодействовать с ним.

Вот скриншот начальной страницы приложения. Обратите внимание на синюю полосу вверху под названием «ЖИВОТНЫЕ». Слева нет кнопки «Назад», но справа есть меню-гамбургер. Этот элемент пользовательского интерфейса является NavAndManuBar (см. исходный код для более подробной информации).

Когда пользователь нажимает кнопку гамбургера, открывается меню:

Если пользователь выбирает Cat, страница и модель представления изменяются следующим образом. Обратите внимание, что теперь у нас есть кнопка «Назад».

Если пользователь нажимает «I Am Big», вводимые данные меняются, поэтому видимый контент реагирует следующим образом:

Задний стек

Новый «задний стек» представляет собой список состояния приложения, а не страницы. Это настраиваемый элемент управления, который самоочищается, чтобы предотвратить дублирование. Использование состояния приложения обеспечивает большую гибкость навигации.

Выводы

Понятие «фреймворка» MVVM заманчиво. Но большинство опубликованных систем — это просто кратчайший путь, освобождающий программистов от написания реального кода. Они также серьезно нарушают принципы проектирования C# и SOLID. По иронии судьбы, они также не дают много реального «МОК»: https://marcusts.com/2018/04/09/the-ioc-контейнер-анти-шаблон/.

Жесткие доказательства

я создал Xamarin.Forms мобильное приложение для демонстрации State Machine. Исходный код доступен на GitHub по адресу https://github.com/marcusts/xamarin-forms-annoyances. Решение называется MVVMAntipattern.sln.

Все приведенные здесь фрагменты кода также включены в пример решения, хотя и не используются в демонстрации.

Код публикуется с открытым исходным кодом и без обременений.

Похожие записи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *