Документация по Avalonia UI
< Все темы
Печать

Добавление новых элементов

Когда мы изначально создавали 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».

Оглавление