Adding new Items - Part II
Now we have the "Add new item" view appearing we need to make it work. In particular we need to
enable/disable the OK button depending on whether the user has typed anything in the Description
.
Implement the OK and Cancel commands
In the last section we bound a Button.Command
to a method on the view model, but if we want to be
able to control the enabled state of the button we need to bind to an
ICommand
. Again we're going to take advantage of ReactiveUI
and use ReactiveCommand
.
ViewModels\AddItemViewModel.cs
using System.Reactive;
using ReactiveUI;
using Todo.Models;
namespace Todo.ViewModels
{
class AddItemViewModel : ViewModelBase
{
string description;
public AddItemViewModel()
{
var okEnabled = this.WhenAnyValue(
x => x.Description,
x => !string.IsNullOrWhiteSpace(x));
Ok = ReactiveCommand.Create(
() => new TodoItem { Description = Description },
okEnabled);
Cancel = ReactiveCommand.Create(() => { });
}
public string Description
{
get => description;
set => this.RaiseAndSetIfChanged(ref description, value);
}
public ReactiveCommand<Unit, TodoItem> Ok { get; }
public ReactiveCommand<Unit, Unit> Cancel { get; }
}
}
First we modify the Description
property to raise
change notifications. We saw this pattern
before in the main window view
model. In this case though, we're implementing change notifications for ReactiveUI
rather than
for Avalonia specifically:
var okEnabled = this.WhenAnyValue(
x => x.Description,
x => !string.IsNullOrWhiteSpace(x));
Now that Description
has change notifications enabled we can use
WhenAnyValue
to convert the property into a
stream of values in the form of an
IObservable
.
This above code can be read as:
- For the initial value of
Description
, and for subsequent changes - Select the inverse of the result of invoking
string.IsNullOrWhiteSpace()
with the value
This means that okEnabled
represents a stream of bool
values which will produce true
when
Description
is a non-empty string and false
when it is an empty string. This is exactly how we
want the OK
button to be enabled.
We then create a ReactiveCommand
and assign it to the Ok
property:
Ok = ReactiveCommand.Create(
() => new TodoItem { Description = Description },
okEnabled);
The second parameter to ReactiveCommand.Create
controls the enabled state of the command, and
so the observable we just created is passed there.
The first parameter is a lambda that is run when the command is executed. Here we simply create
an instance of our model TodoItem
with the description entered by the user.
We also create a command for the "Cancel" button:
Cancel = ReactiveCommand.Create(() => { });
The cancel command is always enabled so we don't pass an observable to control its state, we just pass an "execute" lambda which in this case does nothing.
Bind the OK and Cancel buttons
We can now bind the OK and Cancel buttons in the view to the Ok
and Cancel
commands we just
created on the view model:
Views/AddItemView.axaml
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="200" d:DesignHeight="300"
x:Class="Todo.Views.AddItemView">
<DockPanel>
<Button DockPanel.Dock="Bottom" Command="{Binding Cancel}">Cancel</Button>
<Button DockPanel.Dock="Bottom" Command="{Binding Ok}">OK</Button>
<TextBox AcceptsReturn="False"
Text="{Binding Description}"
Watermark="Enter your TODO"/>
</DockPanel>
</UserControl>
If you run the application and go to the "Add Item" view you should now see that the OK button is
only enabled when text has been entered in the description TextBox
.
Handle the OK and Cancel button
We now need to react to the OK or Cancel buttons being pressed and re-show the list. If OK was
pressed we also need to add the new item to the list. We'll implement this functionality in
MainWindowViewModel
:
ViewModels/MainWindowViewModel.cs
using System;
using System.Reactive.Linq;
using ReactiveUI;
using Todo.Models;
using Todo.Services;
namespace Todo.ViewModels
{
class MainWindowViewModel : ViewModelBase
{
ViewModelBase content;
public MainWindowViewModel(Database db)
{
Content = List = new TodoListViewModel(db.GetItems());
}
public ViewModelBase Content
{
get => content;
private set => this.RaiseAndSetIfChanged(ref content, value);
}
public TodoListViewModel List { get; }
public void AddItem()
{
var vm = new AddItemViewModel();
Observable.Merge(
vm.Ok,
vm.Cancel.Select(_ => (TodoItem)null))
.Take(1)
.Subscribe(model =>
{
if (model != null)
{
List.Items.Add(model);
}
Content = List;
});
Content = vm;
}
}
}
There are a few parts to the added code:
Observable.Merge(vm.Ok, vm.Cancel.Select(_ => (TodoItem)null))
This code takes advantage of the fact that ReactiveCommand
is itself an observable which
produces a value each time the command is executed. You'll notice than when we defined the
commands they had slightly different declarations:
public ReactiveCommand<Unit, TodoItem> Ok { get; }
public ReactiveCommand<Unit, Unit> Cancel { get; }
The second type parameter to ReactiveCommand
specifies the type of result it produces when
the command is executed. Ok
produces a TodoItem
while Cancel
produces a Unit
. Unit
is the reactive version of void
. It means the command produces no value.
Observable.Merge
combines the output
from any number of obervables and merges them into a single observable stream. Because they're
being merged into a single stream, they need to be of the same type. For this reason we call
vm.Cancel.Select(_ => (TodoItem)null)
: this has the effect that every time the Cancel
observable produces a value we select a null
TodoItem
.
.Take(1)
We're only interested in the first click of either the OK or Cancel button; once one of the buttons
are clicked we don't need to listen for any more clicks.
Take(1)
means "just take the first value
produced by the observable sequence".
.Subscribe(model =>
{
if (model != null)
{
List.Items.Add(model);
}
Content = List;
});
Finally we subscribe to the result of the observable sequence. If the command resulted in a model
being produced (i.e. OK was clicked) then add this model to the list. We then set Content
back to
List
in order to display the list in the window and hide the "AddItemView".