DEV Community

loading...
Cover image for Dialogs in AvaloniaUI

Dialogs in AvaloniaUI

Ingvar
・5 min read

Hello, in this post I gonna explain how to create dialogs infrastructure in your Avalonia app! As always I gonna use my app 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

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:

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

public class DialogViewModelBase : DialogViewModelBase<DialogResultBase>
{

}
Enter fullscreen mode Exit fullscreen mode

For views I created similar structure (code here). 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:

public abstract class ParameterizedDialogViewModelBase<TResult, TParameter> : DialogViewModelBase<TResult>
    where TResult : DialogResultBase
    where TParameter : NavigationParameterBase
{
    public abstract void Activate(TParameter parameter);
}
Enter fullscreen mode Exit fullscreen mode

Activate is called when dialog is shown, TParameter is our parameter types that extends empty NavigationParameterBase class. Full VM code is here. 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:

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;
}
Enter fullscreen mode Exit fullscreen mode

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.

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:

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);
Enter fullscreen mode Exit fullscreen mode

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.

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);
Enter fullscreen mode Exit fullscreen mode

Bind view and view model

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

private static void Bind(IDataContextProvider window, object viewModel) => window.DataContext = viewModel;
Enter fullscreen mode Exit fullscreen mode

Activate dialog

If view model supports activation, I call Activate method.


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!");
}
Enter fullscreen mode Exit fullscreen mode

Show dialog

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

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;
}
Enter fullscreen mode Exit fullscreen mode

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

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

public void HideOverlay()
{
    OverlayGrid.ZIndex = -1;
}
Enter fullscreen mode Exit fullscreen mode

This Grid has following style:

<Style Selector="Grid#OverlayGrid">
    <Setter Property="ZIndex" Value="-1" />
    <Setter Property="Background" Value="{DynamicResource DialogOverlayBrush}" />
    <Setter Property="Opacity" Value="0.2" />
</Style>
Enter fullscreen mode Exit fullscreen mode

Return result

Easy part. Avalonia supports dialogs results:

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

// some code here

return result;
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (0)