Dialogs in AvaloniaUI
Tags: dotnet, avaloniaui, csharp
Published: 2021-01-06 21:59:35

Hello, in this post I gonna explain how to create dialogs infrastructure in your Avalonia app! As always I gonna use my app [Camelot](https://github.com/IngvarX/Camelot) as an example of implementation.

# Why do you need dialogs

Why do we need dialogs? Sometimes apps contain functionality that could be shown in separate window. Another option is to replace part of main window with this content, but sometimes it's not possible or doesn't look good from UI/UX side. In this case dialogs could help

# How typical dialog looks like

Typical dialog looks like a small window shown in front of main window. In most implementations background of main window is blurred or semi-transparent that indicates that main window is not active at the moment. Here is an example from Camelot:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/pibaroskc81eal2x3jzg.png)

Also please note that dialog window is centered relatively to parent window. Also dialog is not always just a window, it could be a selector that returns some value to parent window (for example, create directory dialog returns new directory name).

# Dialog view and view model requirements

Dialog view is not a simple user control because dialog itself is a window. Also it requires communication between view and view model because dialog can be closed from vm code. For this purpose I added `Close` request to base dialog view model:

```csharp
public class DialogViewModelBase<TResult> : ViewModelBase
    where TResult : DialogResultBase
{
    public event EventHandler<DialogResultEventArgs<TResult>> CloseRequested;

    protected void Close() => Close(default);

    protected void Close(TResult result)
    {
        var args = new DialogResultEventArgs<TResult>(result);

        CloseRequested.Raise(this, args);
    }
}
```

Here `TResult` represents type of dialog result. I created empty `DialogResultBase` that should be inherited by specific result but you can omit this step. Also I created class for dialog without result:

```csharp
public class DialogViewModelBase : DialogViewModelBase<DialogResultBase>
{

}
```

For views I created similar structure ([code here](https://github.com/IngvarX/Camelot/blob/master/src/Camelot/Views/Dialogs/DialogWindowBase.cs)). Dialog view centers itself and subscribes view model events.

What if we want to pass some parameter to dialog before opening? For example, if I want to create new directory in current one, I will pass current directory path to create directory dialog because it validates if directory name is correct and doesn't exist. For this purpose I added thing called navigation parameter. I added base class for such dialog too:

```csharp
public abstract class ParameterizedDialogViewModelBase<TResult, TParameter> : DialogViewModelBase<TResult>
    where TResult : DialogResultBase
    where TParameter : NavigationParameterBase
{
    public abstract void Activate(TParameter parameter);
}
```

`Activate` is called when dialog is shown, `TParameter` is our parameter types that extends empty `NavigationParameterBase` class. Full VM code is [here](https://github.com/IngvarX/Camelot/blob/master/src/Camelot.ViewModels/Implementations/Dialogs/ParameterizedDialogViewModelBase.cs). Note that this view model doesn't need specific view to work, all navigation parameter logic is done on backend side.

# Dialog service interface

Let's encapsulate dialogs opening logic in separate dialog service. Interface for it looks like this:

```csharp
public interface IDialogService
{
    Task<TResult> ShowDialogAsync<TResult>(string viewModelName)
        where TResult : DialogResultBase;

    Task ShowDialogAsync(string viewModelName);

    Task ShowDialogAsync<TParameter>(string viewModelName, TParameter parameter)
        where TParameter : NavigationParameterBase;

    Task<TResult> ShowDialogAsync<TResult, TParameter>(string viewModelName, TParameter parameter)
        where TResult : DialogResultBase
        where TParameter : NavigationParameterBase;
}
```

Note that it supports result and navigation parameter as generic overloads and accepts view model name. View model name is mandatory. I use it to determine which dialog should be opened (different dialogs could have same results and navigation parameters so there is no other way to understand which one should be used).

I put `IDialogService` into view models project because view models open dialogs. But implementation itself is done in UI project because it depends on some Avalonia-specific things, view models shouldn't care about this.

# Dialog service implementation

How to implement this interface? Let's dive deeper inside implementation. Full code is available [here](https://github.com/IngvarX/Camelot/blob/master/src/Camelot/Services/Implementations/DialogService.cs).

Typically I have 6 steps inside dialog service:
1) Create view
2) Create view model
3) Bind view and view model
4) Activate dialog with navigation parameter
5) Show dialog and wait for result (also here I show/hide overlay on main window)
6) Return dialog result if needed

Looks complicated? Let's go through it step by step:

### Create view

I have naming convention that view model and view have almost similar names, like `CreateDirectoryDialogViewModel` and  `CreateDirectoryDialog`. It allows me to find view by view model type name easily:

```csharp
private static DialogWindowBase<TResult> CreateView<TResult>(string viewModelName)
    where TResult : DialogResultBase
{
    var viewType = GetViewType(viewModelName);
    if (viewType is null)
    {
        throw new InvalidOperationException($"View for {viewModelName} was not found!");
    }

    return (DialogWindowBase<TResult>) GetView(viewType);
}

private static Type GetViewType(string viewModelName)
{
    var viewsAssembly = Assembly.GetExecutingAssembly();
    var viewTypes = viewsAssembly.GetTypes();
    var viewName = viewModelName.Replace("ViewModel", string.Empty);

    return viewTypes.SingleOrDefault(t => t.Name == viewName);
}

private static object GetView(Type type) => Activator.CreateInstance(type);
```

Note that I use reflection for creating instance of view.

### Create view model

Flow here is similar. Note that here I use `Locator` to create instance of view model. It's required because dialogs often have dependencies. You should register your view model with Splat to make this code work. See more about dependency injection in AvaloniaUI in my [previous blog post](https://dev.to/ingvarx/avaloniaui-dependency-injection-4aka).

```csharp
private static DialogViewModelBase<TResult> CreateViewModel<TResult>(string viewModelName)
    where TResult : DialogResultBase
{
    var viewModelType = GetViewModelType(viewModelName);
    if (viewModelType is null)
    {
        throw new InvalidOperationException($"View model {viewModelName} was not found!");
    }

    return (DialogViewModelBase<TResult>) GetViewModel(viewModelType);
}

private static Type GetViewModelType(string viewModelName)
{
    var viewModelsAssembly = Assembly.GetAssembly(typeof(ViewModelBase));
    if (viewModelsAssembly is null)
    {
        throw new InvalidOperationException("Broken installation!");
    }

    var viewModelTypes = viewModelsAssembly.GetTypes();

    return viewModelTypes.SingleOrDefault(t => t.Name == viewModelName);
}

private static object GetViewModel(Type type) => Locator.Current.GetRequiredService(type);
```

### Bind view and view model

Bind is super simple. I set `DataContext` property of dialog with my view model:

```csharp
private static void Bind(IDataContextProvider window, object viewModel) => window.DataContext = viewModel;
```

### Activate dialog

If view model supports activation, I call `Activate` method.

```csharp

switch (viewModel)
{
    case ParameterizedDialogViewModelBase<TResult, TParameter> parameterizedDialogViewModelBase:
        parameterizedDialogViewModelBase.Activate(parameter);
        break;
    case ParameterizedDialogViewModelBaseAsync<TResult, TParameter> parameterizedDialogViewModelBaseAsync:
        await parameterizedDialogViewModelBaseAsync.ActivateAsync(parameter);
        break;
    default:
        throw new InvalidOperationException(
            $"{viewModel.GetType().FullName} doesn't support passing parameters!");
}
```

### Show dialog

This part is a bit tricky. I have to get main window, show overlay on it, show dialog and hide overlay:

```csharp
private async Task<TResult> ShowDialogAsync<TResult>(DialogWindowBase<TResult> window)
    where TResult : DialogResultBase
{
    var mainWindow = (MainWindow) _mainWindowProvider.GetMainWindow();
    window.Owner = mainWindow;

    mainWindow.ShowOverlay();
    var result = await window.ShowDialog<TResult>(mainWindow);
    mainWindow.HideOverlay();
    if (window is IDisposable disposable)
    {
        disposable.Dispose();
    }

    return result;
}
```

Overlay is a semi-transparent `Grid`. Show/hide methods just change `ZIndex` for it:

```csharp
public void ShowOverlay()
{
    OverlayGrid.ZIndex = 1000;
}

public void HideOverlay()
{
    OverlayGrid.ZIndex = -1;
}
```

This `Grid` has following style:

```xml
<Style Selector="Grid#OverlayGrid">
    <Setter Property="ZIndex" Value="-1" />
    <Setter Property="Background" Value="{DynamicResource DialogOverlayBrush}" />
    <Setter Property="Opacity" Value="0.2" />
</Style>
```

### Return result

Easy part. Avalonia supports dialogs results:

```csharp
var result = await window.ShowDialog<TResult>(mainWindow);

// some code here

return result;
```

# Conclusion

In this post I explained how to create and use dialogs in your app. Do you have dialogs in your app? Tell me in comments.