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 | 🐔 Normal
| MVVM, Dialog, FileDialogs, TopLevel, Clipboard | MVVM, Dialog, FileDialogs, TopLevel, Clipboard
| link:src\Avalonia.Samples\ViewInteraction\AdvancedMvvmDialogSample[Advanced Dialog Sample] | link:src\Avalonia.Samples\ViewInteraction\DialogManagerSample[Dialog Manager Sample]
| 🐉 Hard | 🐉 Hard
| MVVM, Dialog, FileDialogs, TopLevel, Clipboard, DialogManager | MVVM, Dialog, FileDialogs, TopLevel, Clipboard, DialogManager

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

@ -40,7 +40,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Folder", "Solution
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MvvmDialogSample", "ViewInteraction\MvvmDialogSample\MvvmDialogSample.csproj", "{48432457-6A55-4D03-9D40-260CE8E06440}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MvvmDialogSample", "ViewInteraction\MvvmDialogSample\MvvmDialogSample.csproj", "{48432457-6A55-4D03-9D40-260CE8E06440}"
EndProject 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 EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Testing", "Testing", "{85B157B3-F701-4F75-B4F1-EC2287729480}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Testing", "Testing", "{85B157B3-F701-4F75-B4F1-EC2287729480}"
EndProject 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" <Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:AdvancedMvvmDialogSample" xmlns:local="using:DialogManagerSample"
RequestedThemeVariant="Light" RequestedThemeVariant="Light"
x:Class="AdvancedMvvmDialogSample.App"> x:Class="DialogManagerSample.App">
<Application.Styles> <Application.Styles>
<FluentTheme /> <FluentTheme />

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

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

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

До

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

После

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

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

@ -24,7 +24,7 @@
<PackageReference Include="Avalonia.Desktop" Version="11.0.0" /> <PackageReference Include="Avalonia.Desktop" Version="11.0.0" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.--> <!--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 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="Avalonia.Themes.Fluent" Version="11.0.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

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

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

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

@ -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 --- // --- D O N ' T T O U C H T H I S S E C T I O N ---
:toc: :toc:
:toc-placement!: :toc-placement!:
@ -12,7 +12,7 @@
// Write a short summary here what this examples does // 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 === Difficulty
// Choose one of the below difficulties. You can just delete the ones you don't need. // Choose one of the below difficulties. You can just delete the ones you don't need.
🐉 Hard 🐉 🐔 Normal 🐔
=== Buzz-Words === Buzz-Words
// Write some buzz-words here. You can separate them by ", " // 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. 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 == The Solution : Use a DialogService to show a Dialog
// This is where you explain the possible solution you provide in this sample. // 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, ... // 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. 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 ```cs
public class DialogService : AvaloniaObject public class DialogManager
{ {
private static readonly Dictionary<object, Visual> RegistrationMapper = private static readonly Dictionary<object, Visual> RegistrationMapper =
new Dictionary<object, Visual>(); 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 ```cs
/// <summary> /// <summary>
/// This property handles the registration of Views and ViewModel /// This property handles the registration of Views and ViewModel
/// </summary> /// </summary>
public static readonly AttachedProperty<object?> RegisterProperty = AvaloniaProperty.RegisterAttached<DialogService, Visual, object?>( public static readonly AttachedProperty<object?> RegisterProperty = AvaloniaProperty.RegisterAttached<DialogManager, Visual, object?>(
"Register"); "Register");
/// <summary> /// <summary>
@ -91,28 +93,28 @@ Now that we want to store or remove any new binding into our `Dictionary`, we ne
[source,cs] [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 // 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 // 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 === 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] [source,xml]
---- ----
<UserControl xmlns="https://github.com/avaloniaui" <UserControl xmlns="https://github.com/avaloniaui"
xmlns:dialog="using:AdvancedMvvmDialogSample.Services" xmlns:services="using:DialogManagerSample.Services"
dialog:DialogService.Register="{Binding}"> services:DialogManager.Register="{Binding}">
<!-- Your content goes here --> <!-- Your content goes here -->
</UserControl> </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#] [source,c#]
---- ----
[RelayCommand]
private async Task SelectFilesAsync() private async Task SelectFilesAsync()
{ {
// Selected Files is a property of our ViewModel // 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: Now we can add the needed `List` and `Button` in our view:
[source,xml] [source,xml]

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

@ -6,42 +6,42 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace AdvancedMvvmDialogSample.Services namespace DialogManagerSample.Services
{ {
public class DialogService : AvaloniaObject public class DialogManager
{ {
private static readonly Dictionary<object, Visual> RegistrationMapper = private static readonly Dictionary<object, Visual> RegistrationMapper =
new Dictionary<object, Visual>(); 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 // 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 // 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> /// <summary>
/// This property handles the registration of Views and ViewModel /// This property handles the registration of Views and ViewModel
/// </summary> /// </summary>
public static readonly AttachedProperty<object?> RegisterProperty = AvaloniaProperty.RegisterAttached<DialogService, Visual, object?>( public static readonly AttachedProperty<object?> RegisterProperty = AvaloniaProperty.RegisterAttached<DialogManager, Visual, object?>(
"Register"); "Register");
/// <summary> /// <summary>
@ -102,7 +102,7 @@ namespace AdvancedMvvmDialogSample.Services
} }
// lookup the TopLevel for the context // lookup the TopLevel for the context
var topLevel = DialogService.GetTopLevelForContext(context); var topLevel = DialogManager.GetTopLevelForContext(context);
if(topLevel != null) 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" <Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 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:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 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" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="AdvancedMvvmDialogSample.Views.MainWindow" x:Class="DialogManagerSample.Views.MainWindow"
x:DataType="vm:MainWindowViewModel" x:DataType="vm:MainWindowViewModel"
x:CompileBindings="True" x:CompileBindings="True"
dialog:DialogService.Register="{Binding}" services:DialogManager.Register="{Binding}"
Icon="/Assets/avalonia-logo.ico" Icon="/Assets/avalonia-logo.ico"
Title="AdvancedMvvmDialogSample"> Title="DialogManagerSample">
<Design.DataContext> <Design.DataContext>
<vm:MainWindowViewModel/> <vm:MainWindowViewModel/>

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

@ -1,6 +1,6 @@
using Avalonia.Controls; using Avalonia.Controls;
namespace AdvancedMvvmDialogSample.Views namespace DialogManagerSample.Views
{ {
public partial class MainWindow : Window 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: 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] * https://github.com/AvaloniaCommunity/awesome-avalonia#mvvm--mvp--mvu[third party libs]