📜 ⬆️ ⬇️

Command-Oriented Navigation in Xaml Applications

Recently, we have already learned about the principle of direct injection and the effective binding of view models to ideas, as well as how to create binding extensions . Let's continue the research of the Aero Framework library and consider another architectural issue.

Navigating between views (screens) in xaml-oriented applications is quite an important and interesting task. This is especially true of its implementation in the framework of the MVVM pattern. Ideally, the view models should not contain any direct references to the representations in order to be cross-platform and retain the possibility of reusing them in several projects. Today we will learn how to achieve this.


The concept of commands ( Commands ) is very closely related to xaml development. Today we will not consider the implementation of this pattern in the Aero Framework library, since it is intuitive and is already briefly covered in the documentation .

Despite all the architectural significance of the issue, there is an amazingly simple and reliable way to navigate ... You may not even believe right away that it is so simple.
')
In most cases, navigation occurs after clicking on a visual element and executing a command on it, so why not pass the identifier (address or type) of the target view as a command parameter? After all, it is very easy and logical.

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

The ability to navigate to one or another view is governed by the CanExecute event at the team, and the Navigator class is responsible for technical details. That is, the target address is only forwarded through the view model.

A slightly more complicated navigation scenario would look like this:

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

After navigation, the presentation using the direct injection mechanism gets the necessary view models from the container.

All this is partly reminiscent of web navigation, where each page is requested from the server by uri . But how in our case to transfer parameters? In connection with the application of the principle of direct injections, there is no real need to transfer any parameters, because each presentation, in fact, can access any view model and extract the necessary information directly from it!

Maybe you just want to come up with a tricky example, but what if you can switch to several views and you don’t know in advance which one ... But all this can be solved simply in several ways: you can create a series of buttons, each with a unique view ID in the command parameter, and depending on the logical conditions, these buttons are designed and / or hidden; otherwise, it is possible to create a converter and bind to the property CommandParameter .

There are a lot of different variations that can be invented, but the idea itself remains unchanged - the ID of the desired view is passed in the command parameter when navigating. But maybe someone will object, but what if you need to pass another parameter to the command? However, there is a way out, you can easily pass a lot of arguments to the command:

 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}"> 

The concept is as follows : they are aware of other views (screens) to which navigation can occur, and the very possibility of switching to a particular view is determined by the logic and state of the view model. The view model does not have any links to the visual interface, and the identifier of the next screen as a command parameter is forwarded through it to the class Navigator, which is already responsible for the technical implementation of the navigation mechanism on a particular platform. The need for the transfer of parameters is eliminated, because, thanks to the principle of direct injection, each view already has access to almost every view model.

As a result, we get clean view models and quite transparent navigation logic and, importantly, reliable. Perhaps, in some cases, you still have to use the Behaind code, but the author of the article failed to come up with a single life example for this, which would not be solved with the help of the considered approach.

Thank you very much for your attention! If you have questions and alternative opinions, then freely express them in the comments!

Source: https://habr.com/ru/post/254615/


All Articles