The Views
In Part 1 we set up the view models for our simple Github client. In this part, we’ll create the views to display these view models.
First of all comes MainWindow.xaml. It’s pretty simple, consisting of only a ReactiveUI ViewModelViewHost control. We briefly mentioned this control last time but to recap, it’s basically an analog of WPF’s ContentControl. You pass a ViewModel into it’s ViewModel property and it tries to look up a corresponding view to display based on the type of the view model.
<Window x:Class="ReactiveGitHubClient.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:xaml="clr-namespace:ReactiveUI.Xaml;assembly=ReactiveUI.Xaml" Title="GitHub with ReactiveUI"> <xaml:ViewModelViewHost HorizontalContentAlignment="Center" VerticalContentAlignment="Center" ViewModel="{Binding Content}"/> </Window>
As you can see, we add a binding to the Content property we implemented in MainWindowViewModel last time. Note that here we’re using a standard XAML binding. ReactiveUI provides its own binding system which is better than WPF’s in many ways, but we’ll get to that later.
Now in the code behind, we simply set the DataContext as we would in any other MVVM application.
public partial class MainWindow : Window { public MainWindow() { this.InitializeComponent(); this.DataContext = new MainWindowViewModel(); } }
Now we move onto LoginView.xaml:
<UserControl x:Class="ReactiveGitHubClient.Views.LoginView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:wag="clr-namespace:WpfAutoGrid;assembly=WpfAutoGrid"> <StackPanel> <wag:AutoGrid Columns="Auto,180" ChildMargin="2"> <Label>User name</Label> <TextBox Text="{Binding UserName, UpdateSourceTrigger=PropertyChanged}"/> <Label>Password</Label> <TextBox Name="Password"/> </wag:AutoGrid> <Button Name="okButton" HorizontalAlignment="Center" MinWidth="80" Margin="4"> OK </Button> </StackPanel> </UserControl>
Here we’re using the super-useful WpfAutoGrid which makes laying out a grid a little less verbose. There’s a NuGet package for it which makes adding it to a project a breeze. We add a Label and TextBox for the UserName and Password and an OK button. You’ll note here that we’re only binding the UserName TextBox in XAML; we’ll use ReactiveUI’s binding to bind the Password TextBox and OK button so you can see a demo of how that works.
ReactiveUI bindings are set up in the code-behind so let’s take a look at that now:
public partial class LoginView : UserControl, IViewFor<LoginViewModel> { public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register( "ViewModel", typeof(LoginViewModel), typeof(LoginView)); public LoginView() { this.InitializeComponent(); // We need to bind the ViewModel property to the DataContext in order to be able to // use WPF Bindings. Let's use WPF bindings for the UserName property. this.WhenAnyValue(x => x.ViewModel).BindTo(this, x => x.DataContext); // And let's use ReactiveUI bindings for the Password property and the Login command. // Note that we don't need to explicitly pass the control we're binding to here for // Password, as the control is named the same as the property on the view model and // ReactiveUI is intelligent enough to realise we want to bind the Text property. this.Bind(this.ViewModel, x => x.Password); // For the LoginCommand on the other hand, we must use a one way binding and explicitly // specify the control and the property being bound to. this.OneWayBind(this.ViewModel, x => x.LoginCommand, x => x.okButton.Command); } public LoginViewModel ViewModel { get { return (LoginViewModel)this.GetValue(ViewModelProperty); } set { this.SetValue(ViewModelProperty, value); } } object IViewFor.ViewModel { get { return this.ViewModel; } set { this.ViewModel = (LoginViewModel)value; } } }
The first thing to notice is that LoginView implements the IViewFor<LoginViewModel> interface. This is the mechanism by which ViewModelViewHost finds a view for a view model. To implement this interface we need to provide a ViewModel property which we implement here as a standard dependency property. The generic IViewFor<T> interface derives from the non-generic IViewFor interface - whose ViewModel property returns a simple object - so we need to also implement that using an explicit interface member.
Now let’s take a look at the 3 lines in the constructor after the standard InitializeComponent call:
this.WhenAnyValue(x => x.ViewModel).BindTo(this, x => x.DataContext);
This first line is necessary because we’re using WPF’s binding system for the UserName TextBox. You’ll notice that IViewFor uses the ViewModel property to specify the bound ViewModel rather than DataContext. This is because DataContext is a XAML-family-specific property and ReactiveUI supports other UI frameworks. Because we want to use WPF’s binding here, we need to keep the two in sync. This line binds LoginViewModel.ViewModel to DataContext so that whenever the former is changed, the change is automatically reflected in the latter. You’ll come to realise that this type of binding can be very useful.
// And let's use ReactiveUI bindings for the Password property and the Login command. // Note that we don't need to explicitly pass the control we're binding to here for // Password, as the control is named the same as the property on the view model and // ReactiveUI is intelligent enough to realise we want to bind the Text property. this.Bind(this.ViewModel, x => x.Password);
This line sets up the ReactiveUI binding we’ve heard so much about. I’ve left the comment in there because I think it just about covers everything.
Note that for this automatic binding to work, the bound control has to be named/capitalized the same as the property that it’s bound to. Unfortunately this violates the .NET coding guidelines as controls are added to the class as fields, which should use camelCase capitalization. This is only one of a few places where ReactiveUI violates .NET conventions, which is something I personally find very irritating, being a stickler for code style! (Note the use of StyleCop in the GitHub project! ;) However, the next line shows us the workaround.
this.OneWayBind(this.ViewModel, x => x.LoginCommand, x => x.okButton.Command);
Here we’re binding Button.Command to LoginViewModel.LoginCommand. Because the LoginCommand property is read-only, we set up the binding to be one-way from the ViewModel to the Button. You can see in the third parameter to OneWayBind that the Button instance and the property on the Button are explicitly supplied; we can also supply this third parameter to the two-way Bind method if we want/need to.
If you run the program now, you’ll find that ReactiveUI throws a NullReferenceException:
An unhandled exception of type 'System.NullReferenceException' occurred in ReactiveUI.Xaml.dll
This isn’t a very helpful message, but what’s happening is that ReactiveUI can’t find a view for the initial value of MainWindowViewModel.Content, which is a LoginViewModel. We need to tell ReactiveUI that LoginView is the view for LoginViewModel. For now, we’ll do this in the App class’ constructor in App.xaml.cs:
public App() { RxApp.MutableResolver.Register(() => new LoginView(), typeof(IViewFor<LoginViewModel>)); }
When ViewModelViewHost tries to look up a view for a view model type TViewModel, it uses ReactiveUI’s dependency resolver to try to find a concrete implementation of IViewFor<TViewModel>. Here we’re using the default dependency resolver to say “when an IViewFor<LoginViewModel> is needed, create a new LoginView”. The ReactiveUI dependency resolver can be overridden with the DI framework of your choice, but for the purposes of this demo we’re going to stick to ReactiveUI’s default.
If you now run the application, you should see a basic login screen:
You’ll notice that the OK button is disabled until a user name and password is entered, as we specified in our view model.
Next time, we’ll make it actually do something, but for now you can find the source on GitHub.
This is a great article series!
ReplyDeleteOne thing that's a good idea, is instead of binding a command via this:
this.OneWayBind(this.ViewModel, x => x.LoginCommand)
Use the "BindCommand" extension method:
this.BindCommand(ViewModel, x => x.LoginCommand, x => x.loginCommand);
This has the benefit of working even for controls that aren't buttons, via the MouseUp event in WPF. Check out the (as yet unfinished) docs for more info:
https://github.com/reactiveui/ReactiveUI/blob/docs/docs/basics/bindings.md#types-of-bindings
This is the best example I've been able to find so far of how to bootstrap up an application using RxUI. The framework is sadly in need of more working examples. Would love to see some further blog posts from you!
ReplyDelete@jcarter documentation is being worked on. Check out issue #687 -> https://github.com/reactiveui/ReactiveUI/issues/687 where we are collating examples of RxUI in the wild.
ReplyDelete