This commit is contained in:
timunie 2023-11-30 20:31:50 +01:00
Родитель 24b2482fef
Коммит f2572c4b74
18 изменённых файлов: 91 добавлений и 108 удалений

Просмотреть файл

@ -161,7 +161,7 @@ Each sample is tagged with it's difficulty. The degree of difficulty describes h
| 🐔 Normal
| MVVM, Dialog, FileDialogs, TopLevel, Clipboard
| link:src\Avalonia.Samples\ViewInteraction\AdvancedMvvmDialogSample[Advanced Dialog Sample]
| link:src\Avalonia.Samples\ViewInteraction\DialogManagerSample[Dialog Manager Sample]
| 🐉 Hard
| MVVM, Dialog, FileDialogs, TopLevel, Clipboard, DialogManager

Просмотреть файл

@ -40,7 +40,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Folder", "Solution
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MvvmDialogSample", "ViewInteraction\MvvmDialogSample\MvvmDialogSample.csproj", "{48432457-6A55-4D03-9D40-260CE8E06440}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdvancedMvvmDialogSample", "ViewInteraction\AdvancedMvvmDialogSample\AdvancedMvvmDialogSample.csproj", "{0BC90E92-D8B3-4C6D-8C47-BAF57CD73CBA}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DialogManagerSample", "ViewInteraction\DialogManagerSample\DialogManagerSample.csproj", "{0BC90E92-D8B3-4C6D-8C47-BAF57CD73CBA}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Testing", "Testing", "{85B157B3-F701-4F75-B4F1-EC2287729480}"
EndProject

Просмотреть файл

@ -1,35 +0,0 @@
using AdvancedMvvmDialogSample.Services;
using ReactiveUI;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows.Input;
namespace AdvancedMvvmDialogSample.ViewModels
{
public class MainWindowViewModel : ViewModelBase
{
public MainWindowViewModel()
{
SelectFilesCommand = ReactiveCommand.CreateFromTask(SelectFilesAsync);
}
private IEnumerable<string>? _SelectedFiles;
/// <summary>
/// Gets or sets a list of Files
/// </summary>
public IEnumerable<string>? SelectedFiles
{
get { return _SelectedFiles; }
set { this.RaiseAndSetIfChanged(ref _SelectedFiles, value); }
}
public ICommand SelectFilesCommand { get; }
private async Task SelectFilesAsync()
{
SelectedFiles = await this.OpenFileDialogAsync("Hello Avalonia");
}
}
}

Просмотреть файл

@ -1,8 +0,0 @@
using ReactiveUI;
namespace AdvancedMvvmDialogSample.ViewModels
{
public class ViewModelBase : ReactiveObject
{
}
}

Просмотреть файл

@ -1,8 +1,8 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:AdvancedMvvmDialogSample"
xmlns:local="using:DialogManagerSample"
RequestedThemeVariant="Light"
x:Class="AdvancedMvvmDialogSample.App">
x:Class="DialogManagerSample.App">
<Application.Styles>
<FluentTheme />

Просмотреть файл

@ -1,10 +1,10 @@
using AdvancedMvvmDialogSample.ViewModels;
using AdvancedMvvmDialogSample.Views;
using DialogManagerSample.ViewModels;
using DialogManagerSample.Views;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
namespace AdvancedMvvmDialogSample
namespace DialogManagerSample
{
public partial class App : Application
{

Просмотреть файл

До

Ширина:  |  Высота:  |  Размер: 172 KiB

После

Ширина:  |  Высота:  |  Размер: 172 KiB

Просмотреть файл

@ -24,7 +24,7 @@
<PackageReference Include="Avalonia.Desktop" Version="11.0.0" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.0" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.0" />
</ItemGroup>
</Project>

Просмотреть файл

@ -1,8 +1,7 @@
using Avalonia;
using Avalonia.ReactiveUI;
using System;
namespace AdvancedMvvmDialogSample
namespace DialogManagerSample
{
internal class Program
{
@ -17,7 +16,6 @@ namespace AdvancedMvvmDialogSample
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.LogToTrace()
.UseReactiveUI();
.LogToTrace();
}
}

Просмотреть файл

@ -1,4 +1,4 @@
= Advanced MVVM Dialog Sample
= Dialog Manager Sample
// --- D O N ' T T O U C H T H I S S E C T I O N ---
:toc:
:toc-placement!:
@ -12,7 +12,7 @@
// Write a short summary here what this examples does
This example will show you how to write a service that will help you to show dialogs in your MVVM application
This example will show you how to write a service (we will call it dialog manager) that will help you to show dialogs in your MVVM application
@ -24,14 +24,14 @@ toc::[]
=== Difficulty
// Choose one of the below difficulties. You can just delete the ones you don't need.
🐉 Hard 🐉
🐔 Normal 🐔
=== Buzz-Words
// Write some buzz-words here. You can separate them by ", "
MVVM, Dialog, FileDialogs, TopLevel, Clipboard, DialogManager
MVVM, Dialog, FileDialogs, TopLevel, Clipboard, DialogManager, CommunityToolkit.MVVM
@ -39,35 +39,37 @@ MVVM, Dialog, FileDialogs, TopLevel, Clipboard, DialogManager
We assume you already now how the MVVM pattern works and how dialogs, such as file dialogs, can be shown in general. You should also know what a https://docs.avaloniaui.net/docs/next/concepts/toplevel[[TopLevel\]]-control in Avalonia is and what it can be used for.
NOTE: In this sample we will use the https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/[[CommunityToolkit.MVVM\]]-package which provides source generators for less boilerplate code. Please check out their docs if you want to learn more about this. (https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/generators/overview)
== The Solution : Use a DialogService to show a Dialog
// This is where you explain the possible solution you provide in this sample.
// If you have more than one option to solve the issue, use Approach 1, Approach 2, ...
=== Step 1: Add a DialogService-class
=== Step 1: Add the DialogManager-class
In our project we add a folder called `Services`. Inside we will create a class called `DialogService` which inherits `AvaloniaObject`.
In our project we add a folder called `Services`. Inside we will create a class called `DialogManager`.
NOTE: We need to inherit `AvaloniaObject` because we want to add `Register` as an `AttachedProperty`
NOTE: We do not need to inherit `AvaloniaObject` even as we want to add `Register` as an `AttachedProperty` because Avalonia supports AttachedProperties on any object
Let's add a static `Dictionary` where we can store the registered mappings between the context (our `ViewModel`) and the `View` or `Control`, which we want to interact with.
```cs
public class DialogService : AvaloniaObject
public class DialogManager
{
private static readonly Dictionary<object, Visual> RegistrationMapper =
new Dictionary<object, Visual>();
}
```
In the next step we add an https://docs.avaloniaui.net/docs/next/concepts/attached-property[[AttachedProperty]] to our dialog service, which we call `Register`. Later we can bind any object to this property from every available `Visual` in our `Views`.
In the next step we add an https://docs.avaloniaui.net/docs/next/concepts/attached-property[[AttachedProperty]] to our dialog manager, which we call `Register`. Later we can bind any object to this property from every available `Visual` in our `Views`.
```cs
/// <summary>
/// This property handles the registration of Views and ViewModel
/// </summary>
public static readonly AttachedProperty<object?> RegisterProperty = AvaloniaProperty.RegisterAttached<DialogService, Visual, object?>(
public static readonly AttachedProperty<object?> RegisterProperty = AvaloniaProperty.RegisterAttached<DialogManager, Visual, object?>(
"Register");
/// <summary>
@ -91,28 +93,28 @@ Now that we want to store or remove any new binding into our `Dictionary`, we ne
[source,cs]
----
static DialogService()
static DialogManager()
{
RegisterProperty.Changed.Subscribe(RegisterChanged);
RegisterProperty.Changed.AddClassHandler<Visual>(RegisterChanged);
}
private static void RegisterChanged(AvaloniaPropertyChangedEventArgs<object?> e)
private static void RegisterChanged(Visual sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Sender is not Visual sender)
if (sender is null)
{
throw new InvalidOperationException("The DialogService can only be registered on a Control");
throw new InvalidOperationException("The DialogManager can only be registered on a Visual");
}
// Unregister any old registered context
if (e.OldValue.Value != null)
if (e.OldValue != null)
{
RegistrationMapper.Remove(e.OldValue.Value);
RegistrationMapper.Remove(e.OldValue);
}
// Register any new context
if (e.NewValue.Value != null)
if (e.NewValue != null)
{
RegistrationMapper.Add(e.NewValue.Value, sender);
RegistrationMapper.Add(e.NewValue, sender);
}
}
----
@ -193,22 +195,23 @@ public static class DialogHelper
=== Step 4: Usage
Now that we have our `DialogService` created, we can start to register the `View` for our `ViewModel`. Thanks to our attached property, we can simply do:
Now that we have our `DialogManager` created, we can start to register the `View` for our `ViewModel`. Thanks to our attached property, we can simply do:
[source,xml]
----
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:dialog="using:AdvancedMvvmDialogSample.Services"
dialog:DialogService.Register="{Binding}">
xmlns:services="using:DialogManagerSample.Services"
services:DialogManager.Register="{Binding}">
<!-- Your content goes here -->
</UserControl>
----
And in the `ViewModel` we can use our extension methods anywhere. The below sample command will ask the user to select a bunch of files:
And in the `ViewModel` we can use our extension methods anywhere. The below sample command will ask the user to select a bunch of files. The command itself will be created using the source generators that the CommunityToolkit.MVVM-package provides.
[source,c#]
----
[RelayCommand]
private async Task SelectFilesAsync()
{
// Selected Files is a property of our ViewModel
@ -216,16 +219,6 @@ private async Task SelectFilesAsync()
}
----
The command itself will be created in the constructor. However you can also use the source generators that the CommunityToolkit.MVVM-package provides.
[source,c#]
----
public MainWindowViewModel()
{
SelectFilesCommand = ReactiveCommand.CreateFromTask(SelectFilesAsync);
}
----
Now we can add the needed `List` and `Button` in our view:
[source,xml]

Просмотреть файл

@ -6,42 +6,42 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace AdvancedMvvmDialogSample.Services
namespace DialogManagerSample.Services
{
public class DialogService : AvaloniaObject
public class DialogManager
{
private static readonly Dictionary<object, Visual> RegistrationMapper =
new Dictionary<object, Visual>();
static DialogService()
static DialogManager()
{
RegisterProperty.Changed.Subscribe(RegisterChanged);
RegisterProperty.Changed.AddClassHandler<Visual>(RegisterChanged);
}
private static void RegisterChanged(AvaloniaPropertyChangedEventArgs<object?> e)
private static void RegisterChanged(Visual sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Sender is not Visual sender)
if (sender is null)
{
throw new InvalidOperationException("The DialogService can only be registered on a Visual");
throw new InvalidOperationException("The DialogManager can only be registered on a Visual");
}
// Unregister any old registered context
if (e.OldValue.Value != null)
if (e.OldValue != null)
{
RegistrationMapper.Remove(e.OldValue.Value);
RegistrationMapper.Remove(e.OldValue);
}
// Register any new context
if (e.NewValue.Value != null)
if (e.NewValue != null)
{
RegistrationMapper.Add(e.NewValue.Value, sender);
RegistrationMapper.Add(e.NewValue, sender);
}
}
/// <summary>
/// This property handles the registration of Views and ViewModel
/// </summary>
public static readonly AttachedProperty<object?> RegisterProperty = AvaloniaProperty.RegisterAttached<DialogService, Visual, object?>(
public static readonly AttachedProperty<object?> RegisterProperty = AvaloniaProperty.RegisterAttached<DialogManager, Visual, object?>(
"Register");
/// <summary>
@ -102,7 +102,7 @@ namespace AdvancedMvvmDialogSample.Services
}
// lookup the TopLevel for the context
var topLevel = DialogService.GetTopLevelForContext(context);
var topLevel = DialogManager.GetTopLevelForContext(context);
if(topLevel != null)
{

Просмотреть файл

@ -0,0 +1,27 @@
using DialogManagerSample.Services;
using System.Collections.Generic;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace DialogManagerSample.ViewModels
{
public partial class MainWindowViewModel : ViewModelBase
{
/// <summary>
/// Gets or sets a list of Files
/// </summary>
[ObservableProperty]
private IEnumerable<string>? _SelectedFiles;
/// <summary>
/// A command used to select some files
/// </summary>
[RelayCommand]
private async Task SelectFilesAsync()
{
SelectedFiles = await this.OpenFileDialogAsync("Hello Avalonia");
}
}
}

Просмотреть файл

@ -0,0 +1,8 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace DialogManagerSample.ViewModels
{
public class ViewModelBase : ObservableObject
{
}
}

Просмотреть файл

@ -1,16 +1,16 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:AdvancedMvvmDialogSample.ViewModels"
xmlns:vm="using:DialogManagerSample.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:dialog="using:AdvancedMvvmDialogSample.Services"
xmlns:services="using:DialogManagerSample.Services"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="AdvancedMvvmDialogSample.Views.MainWindow"
x:Class="DialogManagerSample.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
x:CompileBindings="True"
dialog:DialogService.Register="{Binding}"
services:DialogManager.Register="{Binding}"
Icon="/Assets/avalonia-logo.ico"
Title="AdvancedMvvmDialogSample">
Title="DialogManagerSample">
<Design.DataContext>
<vm:MainWindowViewModel/>

Просмотреть файл

@ -1,6 +1,6 @@
using Avalonia.Controls;
namespace AdvancedMvvmDialogSample.Views
namespace DialogManagerSample.Views
{
public partial class MainWindow : Window
{

Просмотреть файл

До

Ширина:  |  Высота:  |  Размер: 416 KiB

После

Ширина:  |  Высота:  |  Размер: 416 KiB

Просмотреть файл

@ -302,7 +302,7 @@ image::_docs/result_1.png[Custom interaction sample]
There are more ways to show dialogs from the ViewModel, for example:
* link:../AdvancedMvvmDialogSample[Dialog Service]
* link:../DialogManagerSample[DialogManager Service]
* https://github.com/AvaloniaCommunity/awesome-avalonia#mvvm--mvp--mvu[third party libs]