Командно-ориентированная навигация в xaml-приложениях

    Недавно мы уже узнали о принципе прямых инжекции и эффективном связывании вью-моделей с представлениями, а также о том, каким образом создавать расширения привязки. Продолжим исследование библиотеки Aero Framework и рассмотрим ещё один архитектурный вопрос.

    Навигация между представлениями (экранами) в xaml-ориентированных приложениях довольно важная и интересная задача. Особенно это касается её реализации в рамках паттерна MVVM. В идеале вью-модели не должны содержать никаких прямых ссылок на представления, чтобы быть кросс-платформенными и сохранялась возможность повторного их использования в нескольких проектах. Сегодня мы узнаем, как этого достичь.


    С xaml-разработкой очень тесно связана концепция команд (Commands). Сегодня мы не будем рассматривать реализацию этого паттерна в библиотеке Aero Framework, поскольку она интуитивно понятна и уже кратко освещёна в документации.

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

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

    <Button 
        Content="{Localizing About}"
        Command="{Context Key=Navigate}"
        CommandParameter="/Views/AboutView.xaml">
    

    <Button 
        Content="{Localizing About}"
        Command="{Context Key=Navigate}"
        CommandParameter="{x:Type views:PaymentView}">
    

    <Button 
        Content="{Localizing About}"
        Command="{Context Key=Navigate}"
        CommandParameter="http://makeloft.by/">
    

    public class NavigationViewModel : ContextObject, IExposable
    {
        public virtual void Expose()
        {
            this[Context.Navigate].CanExecute += (sender, args) => args.CanExecute = 'any conditions';
            
            this[Context.Navigate].Executed += (sender, args) => Navigator.GoTo(args.Parameter);
        }
    }
    

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

    Немного более сложный сценарий навигации будет выглядеть, например, так:

    <!--LoginView.xaml-->
    <View DataContext="{Store Key=viewModels:LoginViewModel}">
        <StackPanel>
            <TextBlock Text="{Localizing Key=Name}"/>
            <TextBox Text="{Binding Name, Mode=TwoWay}"/>
            <TextBlock Text="{Localizing Key=Password}"/>
            <PasswordBox Password="{Binding Password, Mode=TwoWay}"/>
            
            <Button 
                Command="{Context Key=Login}" 
                CommandParameter="{x:Type ProductsView}" 
                Content="{Localizing Key=Login}"/>
            <TextBlock Text="{Binding Error}"/>
        </StackPanel>
    </View>
    

    [DataContract]
    public class LoginViewModel : ContextObject, IExposable
    {
        [DataMember]
        public string Name
        {
            get { return Get(() => Name); }
            set { Set(() => Name, value); }
        }
        
        public string Password
        {
            get { return Get(() => Name); }
            set { Set(() => Name, value); }
        }
    
        public UserData UserData
        {
            get { return Get(() => UserData); }
            set { Set(() => UserData, value); }
        }
    
        public virtual void Expose()
        {
            this[() => Name].PropertyChanged += (sender, args) => Context.Login.RaiseCanExecuteChanged();
            this[() => Password].PropertyChanged += (sender, args) => Context.Login.RaiseCanExecuteChanged();
            
            this[() => Name].Validation += () =>
                Error = Name == null || Name.Length < 3 
                    ? Unity.App.Localize("InvalidName")
                    : null;
                    
            this[() => Password].Validation += (sender, args) => 
                Error = Name == null || Name.Length < 4 
                    ? Unity.App.Localize("InvalidPassword")
                    : null;
    
            this[Context.Login].CanExecute += (sender, args) =>
            {
                args.CanExecute = string.IsNullOrEmpty(Error); // Error contains last validation fail message
            };
    
            this[Context.Login].Executed += async (sender, args) =>
            {
                try
                {
                    UserData = await Bank.Current.GetUserData();
                    Navigator.GoTo(args.Parameter);
                }
                catch (Exception exception)
                {
                    Error = Unity.App.Localize(exception.Message);
                }
            };
    
            this[Context.Logout].Executed += (sender, args) =>
            {
                UserData = null;
                Navigator.RedirectTo(args.Parameter);
            };
        }
    }
    

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

    Всё это отчасти напоминает веб-навигацию, где каждая страница запрашивается у сервера по uri. Но как же в нашем случае передавать параметры? В связи с применением принципа прямых инжекций отпадает реальная необходимость в передаче каких-либо параметров, ведь каждое представление, по сути, может получить доступ к любой вью-модели и извлечь необходимую информацию непосредственно из неё!

    Возможно, вам сразу захочется придумать каверзный пример, а что если возможен переход на несколько представлений и заранее не известно на какое именно… Но всё это решается элементарно несколькими способами: можно создать ряд кнопок, где у каждой в параметре команды находится уникальный идентификатор представления, и в зависимости от логических условий эти кнопки дизайблятся и/или скрываются; в другом случае допустимо создание конвертера и привязка к свойству CommandParameter.

    Различных вариации может быть придумано очень много, но сама идея остаётся неизменной — идентификатор нужного представления при навигации передаётся в параметре команды. Но, возможно, кто-то возразит, а что если нужно передавать в команду и другой параметр? Однако и тут есть выход, запросто можно передавать множество аргументов в команду:

    public class Set : ObservableCollection<object>
    {
    }
    

    <Set x:Key="ParameterSet">
        <system:String>/Views/AnyView.xaml</system:String>
        <system:String>SecondParaneter</system:String>
    </Set>
    

    <Button 
        Content="{Localizing GoToAnyView}"
        Command="{Context Key=GoTo}"
        CommandParameter="{StaticResource ParameterSet}">
    

    Концепция следующая: представления знают о других представлениях (экранах), на которые может произойти навигация, а сама возможность перехода на то либо иное представление определяется логикой и состоянием вью-модели. Вью-модель же не имеет никаких ссылок на визуальный интерфейс, а идентификатор следующего экрана в качестве параметра команды пробрасывается через неё в класс Navigator, который уже отвечает за техническую реализацию механизма навигации на той или иной платформе. Необходимость в передаче параметров отпадает, поскольку, благодаря принципу прямых инжекции, каждое представление уже имеет доступ практически к любой вью-модели.

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

    Большое спасибо за внимание! Если есть вопросы и альтернативные мнения, то свободно выражайте их в комментариях!
    Метки:
    Поделиться публикацией
    Комментарии 0

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.