Добавление новых элементов
Когда мы изначально создавали TodoListView
, мы добавили кнопку «Добавить элемент». Пришло время заставить эту кнопку что-то делать. При нажатии кнопки мы хотим заменить список элементов новым представлением, которое позволит пользователю ввести описание нового элемента.
Создайте вид
Начнем с создания представления (см. здесь, чтобы вспомнить, как создать UserControl
с помощью шаблона):
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">Cancel</Button> <Button DockPanel.Dock="Bottom">OK</Button> <TextBox AcceptsReturn="True" Text="{Binding Description}" Watermark="Enter your TODO"/> </DockPanel>
Это дает нам представление, которое выглядит следующим образом:
Единственная новая вещь здесь — это элемент управления <TextBox>
, который позволяет пользователю вводить текст. Задаем ему три свойства:
AcceptsReturn
создает многострочныйTextBox
.Text
привязывает текст, отображаемый в текстовом поле, к свойствуDescription
в модели представления.Watermark
вызывает отображение заполнителя, когдаTextBox
пуст.
Создайте модель представления
Наша модель представления будет очень простой. Для начала мы просто предоставим свойство Description
, к которому привязан TextBox
. Мы реализуем это по ходу дела.
ViewModels\AddItemViewModel.cs
namespace Todo.ViewModels { class AddItemViewModel : ViewModelBase { public string Description { get; set; } } }
Смена модели представления списка
Когда мы нажимаем кнопку «Добавить элемент», мы хотим убрать TodoListView
в окне и показать AddItemView
. Мы можем изменить MainWindowViewModel
, чтобы сделать это:
ViewModels/MainWindowViewModel.cs
using ReactiveUI; 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() { Content = new AddItemViewModel(); } } }
Здесь мы добавляем свойство Content
, которое изначально установлено для нашей модели представления списка. Когда вызывается метод AddItem()
, мы присваиваем AddItemViewModel
свойству Content
.
Установщик свойства Content
вызывает RaiseAndSetIfChanged
, что приводит к запуску уведомления об изменении каждый раз, когда значение свойства изменяется. Системе привязки Avalonia нужны уведомления об изменениях, чтобы знать, когда обновлять пользовательский интерфейс в ответ на изменение свойств.
Теперь мы хотим связать наше свойство Window.Content
с этим новым свойством Content
вместо свойства List
, с которым оно в настоящее время связано:
Views/MainWindow.axaml
<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="Todo.Views.MainWindow" Icon="/Assets/avalonia-logo.ico" Width="200" Height="300" Title="Avalonia Todo" Content="{Binding Content}"> </Window>
И, наконец, нам нужно сделать так, чтобы кнопка «Добавить элемент» вызывала MainWindowViewModel.AddItem(
).
Views/TodoListView.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.TodoListView"> <DockPanel> <Button DockPanel.Dock="Bottom" Command="{Binding $parent[Window].DataContext.AddItem}"> Add an item </Button> <ItemsControl Items="{Binding Items}"> <ItemsControl.ItemTemplate> <DataTemplate> <CheckBox Margin="4" IsChecked="{Binding IsChecked}" Content="{Binding Description}"/> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </DockPanel> </UserControl>
Привязка, которую мы добавили к <Button>
:
Command="{Binding $parent[Window].DataContext.AddItem}"
Тут несколько частей:
-
- Свойство
Button.Command
описывает команду, которая будет вызываться при нажатии кнопки. - Мы привязываем его к
$parent[Window].DataContext.AddItem
:$parent[Window]
означает найти предка элемента управления типа Window- И получить его
DataContext
(т.е. в данном случаеMainWindowViewModel
) - И привязать метод
AddItem
к этой модели представления.
- Свойство
Это приведет к вызову метода MainWindowViewModel.AddItem()
при нажатии кнопки.
Если вы знакомы с WPF или UWP, вам может показаться странным, что мы привязываем Button.Command
к методу. Это удобная функция Avalonia, которая означает, что вам не нужно создавать ICommand
для простых команд, которые всегда доступны.
Запустите приложение
Если вы сейчас запустите приложение и нажмете кнопку «Добавить элемент», вы должны увидеть новое представление.
Теперь у нас появилось представление «Добавить новый элемент», и нам нужно, чтобы оно заработало. В частности, нам нужно активировать/деактивировать кнопку OK в зависимости от того, ввел ли пользователь что-либо в Description.
Реализуйте команды OK и Cancel
В последнем разделе мы привязали Button.Command
к методу модели представления, но если мы хотим иметь возможность управлять включенным состоянием кнопки, нам нужно привязать к ICommand
. Мы снова воспользуемся преимуществами ReactiveUI
и используем 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; } } }
Сначала мы модифицируем свойство Description
, чтобы получать уведомления об изменениях. Мы уже видели этот паттерн в модели представления главного окна. Однако в данном случае мы реализуем уведомления об изменениях для ReactiveUI
, а не конкретно для Avalonia:
var okEnabled = this.WhenAnyValue( x => x.Description, x => !string.IsNullOrWhiteSpace(x));
Теперь, когда в Description
включены уведомления об изменениях, мы можем использовать WhenAnyValue
для преобразования свойства в поток значений в форме IObservable
.
Этот приведенный выше код можно прочитать как:
- Для начального значения
Description
и для последующих изменений - Выберите обратный результат вызова
string.IsNullOrWhiteSpace()
со значением
Это означает, что okEnabled
представляет собой поток логических значений, который выдает true
, когда Description
является непустой строкой, и false
, если это пустая строка. Именно так мы хотим, чтобы кнопка OK была включена.
Затем мы создаем ReactiveCommand
и назначаем его свойству Ok
:
Ok = ReactiveCommand.Create( () => new TodoItem { Description = Description }, okEnabled);
Второй параметр ReactiveCommand.Create
управляет состоянием доступности команды, поэтому только что созданный наблюдаемый объект передается туда.
Первый параметр — это лямбда, которая запускается при выполнении команды. Здесь мы просто создаем экземпляр нашей модели TodoItem
с описанием, введенным пользователем.
Также создаем команду для кнопки «Отмена»:
Cancel = ReactiveCommand.Create(() => { });
Команда отмены всегда доступна, поэтому мы не передаем наблюдаемое для управления его состоянием, мы просто передаем лямбду «выполнить», которая в данном случае ничего не делает.
Привяжем кнопки ОК и Отмена
Теперь мы можем связать кнопки OK и Cancel в представлении с командами Ok
и Cancel
, которые мы только что создали в модели представления:
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>
Если вы запустите приложение и перейдете к представлению «Добавить элемент», вы увидите, что кнопка «ОК» активна только тогда, когда текст был введен в текстовом поле описания.
Работа с кнопками OK и Cancel
Теперь нам нужно отреагировать на нажатие кнопок «ОК» или «Отмена» и заново отобразить список. Если было нажато OK, нам также нужно добавить новый элемент в список. Мы реализуем эту функциональность в 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; } } }
Добавленный код состоит из нескольких частей:
Observable.Merge(vm.Ok, vm.Cancel.Select(_ => (TodoItem)null))
В этом коде используется тот факт, что ReactiveCommand
сама является наблюдаемой, которая выдает значение каждый раз, когда команда выполняется. Вы заметите, что когда мы определяли команды, они имели немного разные объявления:
public ReactiveCommand<Unit, TodoItem> Ok { get; } public ReactiveCommand<Unit, Unit> Cancel { get; }
Второй параметр типа для ReactiveCommand
указывает тип результата, который он выдает при выполнении команды. Ok
создает TodoItem
, а Cancel
создает Unit
. Unit
— это реактивная версия void
. Это означает, что команда не производит никакого значения.
Observable.Merge объединяет выходные данные любого количества наблюдаемых объектов и объединяет их в один наблюдаемый поток. Поскольку они объединяются в один поток, они должны быть одного типа. По этой причине мы вызываем vm.Cancel.Select(_ => (TodoItem)null)
: это приводит к тому, что каждый раз, когда наблюдаемая Cancel
создает значение, мы выбираем нулевой TodoItem
.
.Take(1)
Нас интересует только первый клик по кнопке «ОК» или «Отмена»; как только одна из кнопок нажата, нам больше не нужно слушать клики. Take(1) означает «просто взять первое значение, созданное наблюдаемой последовательностью».
.Subscribe(model => { if (model != null) { List.Items.Add(model); } Content = List; });
Наконец, мы подписываемся на результат наблюдаемой последовательности. Если команда привела к созданию модели (т. е. была нажата кнопка «ОК»), добавьте эту модель в список. Затем мы устанавливаем Content
обратно в List
, чтобы отобразить список в окне и скрыть «AddItemView»
.