Перейти к содержанию

IDialogService

IDialogService предоставляет платформенно-независимый способ показывать модальные диалоги. Из ViewModel можно открывать и обрабатывать модальные диалоги с помощью специальных ViewModel диалогов. Благодаря этому ваши ViewModel полностью отделены от типов окон Avalonia.

Основные возможности:

  • Диалоги, управляемые ViewModel — Диалог полностью определяется ViewModel. ViewModel задаёт заголовок, содержимое и кнопки диалога.
  • Простые диалоги — Если нужно показать только обычный текст с набором кнопок, View диалога определять не требуется. Достаточно создать ViewModel диалога, унаследовав её от OkCancelDialogAwareViewModel, и присвоить строку свойству Content этой ViewModel.
  • Собственные View диалогов — Можно создать собственное View, которое отображается как внутреннее содержимое диалога. Это View находится автоматически с помощью атрибута ViewLocatorAttribute, применённого к ViewModel диалога.
  • Настраиваемые наборы кнопок — Базовый класс ViewModel диалога предоставляет стандартные кнопки OK/Cancel, но можно переопределить CreateButtons, чтобы задать любой набор кнопок с собственными подписями и расположением.
  • Поддержка валидации — Переопределите метод CanOk в ViewModel диалога, чтобы кнопка по умолчанию оставалась недоступной, пока введённые данные некорректны.
  • Вложенные модальные окна — Диалог может открыть поверх себя другой модальный диалог.
  • Результат диалога — После закрытия диалог возвращает результат, заданный перечислением DialogResult.

Определение интерфейса

public interface IDialogService
{
    // Show a modal dialog driven by a ViewModel.
    DialogResult ShowDialog<T>(T viewModel, string? caption = null)
        where T : IDialogAwareViewModel;
}
Параметр Тип Описание
viewModel T ViewModel диалога. Она задаёт содержимое и кнопки диалога. ViewModel присваивается Data Context диалога. ViewModel можно создать, унаследовав её от класса OkCancelDialogAwareViewModel, который уже реализует интерфейс IDialogAwareViewModel. OkCancelDialogAwareViewModel содержит кнопки OK и Cancel, которые можно заменить собственным набором кнопок.
caption string? Заголовок диалога. Если задано значение null, заголовок берётся из свойства Title ViewModel диалога (IWindowAwareViewModel.Title).

Возвращаемое значение: DialogResult — кнопка, которой был закрыт диалог, или DialogResult.None, если пользователь закрыл диалог кнопкой в заголовке окна.

Иерархия ViewModel диалогов

ViewModel диалога должна реализовывать интерфейс IDialogAwareViewModel. Как правило, ViewModel диалога наследуют от класса OkCancelDialogAwareViewModel, который уже реализует этот интерфейс. Класс OkCancelDialogAwareViewModel содержит кнопки OK и Cancel, которые можно заменить собственными кнопками.

На следующей схеме показана иерархия базовых классов для ViewModel диалогов:

IWindowAwareViewModel
    └── IDialogAwareViewModel
            └── WindowAwareViewModel (abstract class)
                    └── DialogAwareViewModel (abstract class)
                            └── OkCancelDialogAwareViewModel (abstract class)
Класс Назначение
WindowAwareViewModel Базовый класс для ViewModel, отображаемых в окне. Отслеживает окно, к которому привязана ViewModel, закрывает его по запросу и создаёт View, объявленное атрибутом ViewLocatorAttribute.
DialogAwareViewModel Базовый класс для ViewModel, отображаемых в виде модальных диалогов. Этот класс добавляет поддержку кнопок диалога. Чтобы задать собственный набор кнопок, можно переопределить метод CreateButtons.
OkCancelDialogAwareViewModel Предоставляет стандартные кнопки OK и Cancel, связанные с виртуальными методами OkCancelDialogAwareViewModel.Ok и OkCancelDialogAwareViewModel.Cancel. Метод CanOk можно переопределить, чтобы отключать кнопку OK в соответствии с правилами валидации диалога.

Как использовать IDialogService

Чтобы отобразить модальный диалог, вызовите в ViewModel метод IDialogService.ShowDialog. Метод возвращает результат диалога (кнопку, которую нажал пользователь).

Доступ к сервису

Получить объект IDialogService в ViewModel можно двумя способами:

  • С помощью вспомогательного метода Service<T>(). Это удобный способ получить зарегистрированные сервисы приложения, входящие в библиотеку Eremex Controls.
  • Через внедрение в конструктор.

Вспомогательный метод Service()

Реализуйте в ViewModel вспомогательный метод Service<T>(), который получает запрошенный сервис с помощью статического метода ApplicationServicesContext.GetRequiredService<T>().

using Eremex.AvaloniaUI.Controls.ApplicationServices;
using CommunityToolkit.Mvvm.Input;

public partial class MyViewModel : ObservableObject
{
    // Provides access to any registered service
    protected static T Service<T>() where T : class
        => ApplicationServicesContext.GetRequiredService<T>();

    [RelayCommand]
    private void ShowDialog()
    {
        // Create the dialog view model
        var dialogViewModel = new MyDialogViewModel("Dialog Title", "Dialog content");

        // Resolve the service and show the dialog
        var result = Service<IDialogService>()
            .ShowDialog(dialogViewModel, caption: null);
    }
}

Убедитесь, что в файле App.axaml.cs сервисы приложения Eremex зарегистрированы с помощью SimpleServiceProvider и ApplicationServicesContext следующим образом:

public class App : Application
{
    public override void OnFrameworkInitializationCompleted()
    {
        RegisterApplicationServices();
        //...
    }

    static void RegisterApplicationServices()
    {
        // Register built-in services.
        var serviceProvider = new SimpleServiceProvider();
        ApplicationServicesContext.RegisterApplicationServices(serviceProvider.AddSingleton);
        ApplicationServicesContext.SetCurrent(serviceProvider);
    }
}

Внедрение в конструктор

Реализуйте в ViewModel конструктор с параметром типа IDialogService. При создании ViewModel передайте объект сервиса в этот конструктор.

public partial class MyViewModel
{
    private readonly IDialogService _dialogService;

    public MyViewModel(IDialogService dialogService)
    {
        _dialogService = dialogService;
    }

    [RelayCommand]
    private void ShowDialog()
    {
        var dialogViewModel = new MyDialogViewModel("Dialog Title", "Dialog content");
        var result = _dialogService.ShowDialog(dialogViewModel, caption: null);
        //...
    }
}

Создание ViewModel диалога

ViewModel диалога задаёт его содержимое и кнопки. Проще всего унаследовать её от OkCancelDialogAwareViewModel: этот класс сразу предоставляет кнопки OK и Cancel.

Tip

OkCancelDialogAwareViewModel позволяет заменить кнопки OK и Cancel собственным набором кнопок. Каждая кнопка может закрывать диалог с определённым результатом (значением DialogResult).

В этом примере показано, как отобразить диалог с кнопками OK и Cancel. ViewModel создаётся наследованием от класса OkCancelDialogAwareViewModel. Строки, переданные в конструктор ViewModel, задают заголовок и содержимое диалога.

app-services-idialogservice-ok-cancel

using Eremex.AvaloniaUI.Controls.ApplicationServices;

public class MyDialogViewModel : OkCancelDialogAwareViewModel
{
    public MyDialogViewModel(string title, string message) : base(title)
    {
        Content = message;
    }
}

Чтобы показать этот диалог, используйте следующий код:

var result = Service<IDialogService>()
    .ShowDialog(new MyDialogViewModel("Dialog Title", "Dialog content"));

Собственные наборы кнопок

Чтобы заменить стандартный набор кнопок, переопределите метод CreateButtons в ViewModel диалога. Для создания стандартных кнопок OK, Cancel и Apply можно использовать встроенные фабричные методы (CreateOk, CreateCancel, CreateApply). Для создания собственных кнопок используйте метод CreateCustom.

В этом примере показан диалог с кнопками Yes, No и Cancel. Этот диалог представлен ViewModel DialogServiceYesNoCancelViewModel, унаследованной от класса OkCancelDialogAwareViewModel. Метод CreateButtons переопределён, чтобы создать собственный набор кнопок.

Кнопка Yes назначена кнопкой по умолчанию. Команда кнопки по умолчанию вызывается, когда пользователь нажимает ENTER в диалоге. У встроенной кнопки Cancel включён параметр IsCancel. Этот параметр вызывает команду кнопки при нажатии клавиши ESC.

app-services-idialogservice-yes-no-cancel-buttons

using CommunityToolkit.Mvvm.Input;
using Eremex.AvaloniaUI.Controls.ApplicationServices;

/// <summary>
/// IDialogService with the Yes, No, and Cancel buttons.
/// </summary>
public partial class DialogServiceYesNoCancelViewModel : OkCancelDialogAwareViewModel
{
    public DialogServiceYesNoCancelViewModel(string title, string message) : base(title)
    {
        Content = message;
    }

    [RelayCommand]
    public void Yes(object parameter) => CloseWindow(DialogResult.Yes);

    [RelayCommand]
    public void No(object parameter) => CloseWindow(DialogResult.No);

    protected override IDialogButtonViewModel[] CreateButtons()
    {
        var buttons = new IDialogButtonViewModel[]
        {
            DialogButtonViewModel.CreateCustom(YesCommand, "Yes", classes: "accent"),
            DialogButtonViewModel.CreateCustom(NoCommand, "No"),
            DialogButtonViewModel.CreateCancel(CancelCommand),
        };
        (buttons[0] as DialogButtonViewModel).IsDefault = true;
        return buttons;
    }
}

Чтобы показать этот диалог, используйте следующий код:

var result = Service<IDialogService>().ShowDialog(new DialogServiceYesNoCancelViewModel("Warning", "Save the changes?"));

Собственные подписи и расположение кнопок

Собственные кнопки можно создать, переопределив метод DialogAwareViewModel.CreateButtons и вызвав в нём статический метод DialogButtonViewModel.CreateCustom. Метод CreateCustom позволяет задать подпись и расположение кнопки.

В этом примере показан диалог с кнопками Yes, No и Cancel. Кнопки Yes и No размещены слева от кнопки Cancel с помощью параметра dockType.

using CommunityToolkit.Mvvm.Input;
using Eremex.AvaloniaUI.Controls.ApplicationServices;

public partial class DialogServiceNoYesCancelViewModel : OkCancelDialogAwareViewModel
{
    public DialogServiceNoYesCancelViewModel(string title, string message) : base(title)
    {
        Content = message;
    }

    [RelayCommand]
    public void Yes(object parameter) => CloseWindow(DialogResult.Yes);

    [RelayCommand]
    public void No(object parameter) => CloseWindow(DialogResult.No);

    protected override IDialogButtonViewModel[] CreateButtons() => new[]
    {
        DialogButtonViewModel.CreateCancel(CancelCommand),
        DialogButtonViewModel.CreateCustom(NoCommand, "No", dockType: DockType.Left),
        DialogButtonViewModel.CreateCustom(YesCommand, "Yes", dockType: DockType.Left),
    };
}

Чтобы показать этот диалог, используйте следующий код:

var result = Service<IDialogService>().ShowDialog(new DialogServiceNoYesCancelViewModel("Info", "Custom button placement"));

Собственное содержимое диалога и валидация

По умолчанию диалог отображает строку, присвоенную свойству Content ViewModel диалога (WindowAwareViewModel.Content). Вместо простой строки можно также отобразить собственное содержимое.

Чтобы задать собственное содержимое диалога:

  1. Создайте View с контролами, которые нужно отобразить в диалоге.

    Добавьте UserControl с нужными контролами. При необходимости привяжите его свойства к свойствам ViewModel диалога, чтобы они оставались синхронизированными.

  2. Примените к ViewModel диалога атрибут ViewLocatorAttribute.

    Пометьте ViewModel диалога атрибутом ViewLocatorAttribute, передав ему тип созданного View. Атрибут связывает View с ViewModel. Сервис диалогов использует его, чтобы создать и отобразить View в клиентской области диалога (над кнопками в нижней части диалога).

Если атрибут ViewLocatorAttribute задан, базовый класс WindowAwareViewModel автоматически создаёт View при открытии диалога. Это View присваивается свойству Content. ViewModel никогда не нужно ссылаться на тип View напрямую.

Tip

Свойство Content ViewModel диалога можно также задать вручную, без использования ViewLocatorAttribute.

  1. (Необязательно) Управляйте доступностью кнопки OK диалога

    Чтобы включать и отключать кнопку OK в соответствии с вашими требованиями, переопределите метод CanOk в ViewModel диалога.

Пример

В этом примере показан диалог с текстовым полем и кнопками OK/Cancel. Текстовое поле находится в собственном View (DialogServiceValidationView). Метод CanOk переопределён, чтобы кнопка OK оставалась недоступной, пока текстовое поле пусто.

ViewModel

using Eremex.AvaloniaUI.Controls.ApplicationServices;

namespace DemoCenter.ViewModels.ApplicationServices;

// The Ok button stays disabled until the UserName field is filled in.
[ViewLocator(typeof(DialogServiceValidationView))]
public class DialogServiceValidationViewModel : OkCancelDialogAwareViewModel
{
    private string userName = string.Empty;

    public DialogServiceValidationViewModel(string title) : base(title)
    {
    }

    public string UserName
    {
        get => userName;
        set
        {
            if (userName == value)
                return;

            userName = value;
            OnPropertyChanged(nameof(UserName));
            OkCommand.NotifyCanExecuteChanged();
        }
    }

    public override bool CanOk(object parameter) => !string.IsNullOrWhiteSpace(UserName);
}

View

<UserControl x:Class="DemoCenter.Views.ApplicationServices.DialogServiceValidationView"
             xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mxe="https://schemas.eremexcontrols.net/avalonia/editors"
             xmlns:vm="using:DemoCenter.ViewModels.ApplicationServices"
             x:CompileBindings="True"
             x:DataType="vm:DialogServiceValidationViewModel"
             MinWidth="380">

    <StackPanel Margin="20" Spacing="10">
        <TextBlock Text="The Ok button stays disabled until the field is filled in."
                   TextWrapping="Wrap" />
        <mxe:TextEditor EditorValue="{Binding UserName, Mode=TwoWay}" />
    </StackPanel>
</UserControl>

Отображение диалога

Чтобы показать этот диалог, используйте следующий код:

var result = Service<IDialogService>().ShowDialog(new DialogServiceValidationViewModel("Custom dialog content and validation"));

Вложенные модальные диалоги

Диалог может открыть поверх себя другой модальный диалог.

В этом примере показан диалог с кнопкой "Nested dialog". Нажатие этой кнопки отображает ещё один диалог.

using CommunityToolkit.Mvvm.Input;
using Eremex.AvaloniaUI.Controls.ApplicationServices;

// The primary dialog ViewModel, which contains a button that invokes a nested dialog.
public partial class DialogServiceNestedViewModel : OkCancelDialogAwareViewModel
{
    private readonly IDialogService dialogService;

    public DialogServiceNestedViewModel(string title, IDialogService dialogService) : base(title)
    {
        this.dialogService = dialogService;
        Content = "Press \"Nested dialog\" to open a modal dialog on top of this one. "
            + "The nested dialog blocks this window only.";
    }

    [RelayCommand]
    public void ShowNested(object parameter)
    {
        var nested = new DialogServiceOkCancelViewModel(
            "Nested dialog",
            "This modal dialog is owned by the dialog underneath.");

        dialogService.ShowDialog(nested, caption: null);
    }

    protected override IDialogButtonViewModel[] CreateButtons() => new[]
    {
        DialogButtonViewModel.CreateCustom(ShowNestedCommand, "Nested dialog", dockType: DockType.Left),
        DialogButtonViewModel.CreateOk(OkCommand),
        DialogButtonViewModel.CreateCancel(CancelCommand),
    };
}

// The simplest nested dialog ViewModel
public class DialogServiceOkCancelViewModel : OkCancelDialogAwareViewModel
{
    public DialogServiceOkCancelViewModel(string title, string message) : base(title)
    {
        Content = message;
    }
}

Чтобы показать этот диалог, используйте следующий код:

var result = Service<IDialogService>().ShowDialog(new DialogServiceNestedViewModel("Dialog that shows a nested dialog", Service<IDialogService>()));

Выделение кнопки акцентом

Любую кнопку диалога можно выделить акцентом, чтобы она визуально выделялась. Например, можно выделить кнопку по умолчанию.

Tip

Кнопка по умолчанию — это кнопка, которая срабатывает, когда пользователь нажимает ENTER.

Чтобы выделить кнопку акцентом, укажите "accent" в параметре classes метода CreateCustom.

В этом примере акцентом выделена кнопка Yes. У этой кнопки включено свойство IsDefault, чтобы назначить её кнопкой диалога по умолчанию.

    protected override IDialogButtonViewModel[] CreateButtons()
    {
        var buttons = new IDialogButtonViewModel[]
        {
            DialogButtonViewModel.CreateCustom(YesCommand, "Yes", classes: "accent"),
            DialogButtonViewModel.CreateCustom(NoCommand, "No"),
            DialogButtonViewModel.CreateCancel(CancelCommand),
        };
        (buttons[0] as DialogButtonViewModel).IsDefault = true;
        return buttons;
    }



* Эта страница переведена с использованием технологий машинного перевода.