Самодостаточные контроллы на Xamarin.Forms: «Переиспользуй код на максимум!». Часть 1



    Ещё в качестве идеи Xamarin.Forms понравился всем WPF разработчикам: популярность создания приложений под Android и iOS росла, WPF становился пережитком, а востребованность WPF разработчиков неуклонно стремилась к нулю — Forms звучал, как спасение. Появилась надежда, что мы со своим знанием XAML и паттерна MVVM будем кому-нибудь нужны. Конечно, изначально Xamarin.Forms оказался сырым, с большим количеством багов и отсутствием некоторых жизненно необходимых вещей (вспомнить хотя бы input control без возможности указания maxwith).

    Прошло три года и Microsoft приобрел Xamarin. Теперь он поставляет его вместе с Visual Studio, и как следствие: багов стало меньше, а возможностей из коробки — больше. Но осталась одна проблема: приложения с единым интерфейсом не получаются нативными. То есть, если появляется различие в интерфейсах Android и iOS, разработчик сталкивается с болью в виде создания отдельных ViewModel под каждую платформу…и это только цветочки.

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

    В такой ситуации хочется иметь возможность переиспользовать код не только в разных платформах, но и на разных проектах. Соответственно, нужно предусмотреть самодостаточность контроллов, т.е. контролл, существующий со всеми его функциями бизнес-логики, взаимодействием с REST API, кэшированием и пр. Причем такие контроллы должны быть независимы от наличия других контроллов. Необходимо создавать ситуацию, при которой контролл живет своей жизнью и вызывает (invoke) какие-то ивенты, когда что-то произошло.

    Забегая вперед, отмечу, что выглядит все просто и естесственно, однако такой подход пришел к нам через долгие недели рефакторинга кода, где изначально сильно отделялся слой данных от слоя интерфейса с бизнесслогикой. При таком классическом подходе все здорово: понятно какой модуль за что отвечает, порог входа нового разработчика низок. Однако, когда проект разрастается до нескольких десятков контроллов, каждый из которых взаимодействет с другими — появляются неожиданные ошибки, связанные с запутанностью вызовов методов разных контроллов. Это все, в конечном итоге привел нас к так называемому «аду вьюмоделей».

    Распутывается он благодаря подходу через сервисы и подписки на изменения в этих сервисах. Т.е. мы делаем акцент не на том, чтоб слой данных был абсолютно независимым от логики или интерфейса, а на том, чтоб какой-то конкретный контролл был абсолютно незамисимым от остальных контроллов.

    Сегодня я покажу самый простой пример – авторизацию. Представим, что у нас есть REST точка для авторизации. Мы создаем контролл, способный авторизовывать пользователей во всех системах в компании. Его интерфейс реализуется один раз. Все требования по стилям указываются в разметке в контролле авторизации:

    Разметка контролла авторизации
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <ActivityIndicator Grid.RowSpan="2" VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand" Color="Red" IsEnabled="True" IsVisible="{Binding IsLoading}" IsRunning="True"/>
        <Label Text="Авторизация" FontSize="Large" VerticalOptions="CenterAndExpand" HorizontalOptions="CenterAndExpand"/>
        <StackLayout Grid.Row="1" Orientation="Horizontal">
            <Entry Text="{Binding Login, Mode=TwoWay}" VerticalOptions="CenterAndExpand" HorizontalOptions="FillAndExpand"/>
            <Button Text="OK" VerticalOptions="CenterAndExpand" Command="{Binding RegisterCommand}" BackgroundColor="Red" TextColor="White"/>
        </StackLayout>
    </Grid>

    Верстка такого контролла — самая сложная часть его создания: надо учитывать то, что он должен быть адаптивным к любому из контейнеров, где в последствии может оказаться. Буквально надо остерегаться любой конкретной величины, пытаясь растягивать контент. Тут нам поможет Grid с его автоматической растяжкой контента по ячейкам и Vertical/HorizontalOptions. К сожалению, ViewBox (контролл, который растягивает контент) из UWP в Forms еще не реализовали, однако такая инициатива была предложена и за нее можно проголосовать на странице разработчиков Xamarin, где пользователи буквально приоритезируют «фичи» которые войдут в следующие билды Xamarin.Forms.

    Затем реализуем ViewModel, в которой вызывается метод сервиса авторизации из бизнес-логики.

    ViewModel регистрации
    IAuthorizationService _authorizationService;
    public AuthorizationViewModel()
    {
        this.RegisterCommand = new Command(async () => { await Registration(); });
        _authorizationService = Core.DI.Container.GetInstance<IAuthorizationService>();
    }
    private async Task Registration()
    {
        IsLoading = true;
        await _authorizationService.Login(Login);
        IsLoading = false;
    }
    private string _login;
    public string Login
    {   get {   return _login;  }
        set {   _login = value; RaizePropertyChanged(nameof(Login));}
    }
    //INotifyPropertyChanged realization

    Использование контролла
    	
    <local:AuthorizationView WidthRequest="300" VerticalOptions="CenterAndExpand" HorizontalOptions="Center"/>

    Подписываться на изменение этого сервиса можно из любого места в приложении и обработать это соответствующим образом. Сервис отвечает только за рейз.

    Подписка на событие контролла
    var authorizationService = Core.DI.Container.GetInstance<IAuthorizationService>();
    authorizationService.AuthorizationChanged += AuthorizationService_ AuthorizationChanged;

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

    Читайте в части 2: более сложные контроллы и их взаимодействие друг с другом.

    Ссылка на github

    image image
    Mobile Dimension
    Mobile Dimension — разработчик мобильных решений
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 6

      0
      Лоадер на айфоне за текстбоксом прячется нижней частью, надо его в самый низ списка контролов в гриде перенести. Странно, что на андроиде при этом все нормально.
      Как там в Xamarin обстоят дела с Rx? Всякие формы и фильтры на Rx значительно удобнее писать.
        +1
        Rx это всего лишь библиотека на .Net, отлично работает с Xamarin, если надо
        +1
        Но осталась одна проблема: приложения с единым интерфейсом не получаются нативными.

        не очень понятно. из-за того, что Xamarin.Forms использует нативные контроли, то и приложения с единым интерфейсом получаются нативными.
        То есть, если появляется различие в интерфейсах Android и iOS, разработчик сталкивается с болью в виде создания отдельных ViewModel под каждую платформу…и это только цветочки.

        Даже если есть различия, то зачем создавать разные ViewModel? В этом и преимущество MVVM, что ViewModel не должен знать как реализовано View — он просто дает доступ к необходимым данным.
          0
          Речь здесь идет о нативных контроллах (в IOS и Android), которые не поддерживаются в Xamarin.
          Решение таких ситуаций заслуживают отдельной статьи.
          Но забегая вперед, можно сказать, что если использовать поля для поддержания состояния View на обоих платформах в одной ViewModel, тогда класс ViewModel разрастется.
          Со временем приходит осознание того, что проще создать отдельные ViewModel для каждой платформы, а общие поля вынести в отдельный сервис.
          Но с замечанием согласен, момент ситуативный.
            0
            если использовать поля для поддержания состояния View на обоих платформах в одной ViewModel

            Во ViewModel не должно быть платформозависимого кода, иначе теряется то самое преимущество MVVM, о котором сказал Don_Eric.
            А если приходится это делать, или писать разные ViewModel, это нужно рассматривать как симптом того, что-то вы делаете не так. Возможно, в ViewModel оказывается часть кода, который должен быть в View (просто предположение).

              0
              Все эти вещи ситуативны и зависят от конкретных условий, которые не поместятся в один комментарий (в некоторых подходах MVVM во View вообще не должно быть кода, за исключением привязки дата контекста, опять же, ситуативно). Мы поняли, что эта тема актуальна, спасибо вам! Развернем подробнее в следующих статьях.

        Only users with full accounts can post comments. Log in, please.