Working on ReadMe for custom Interaction sample
This commit is contained in:
Родитель
a0913c944c
Коммит
af970ca1a2
|
@ -132,6 +132,167 @@ Now we can setup the `XAML` as following. Mind the binding of the `Button` to ou
|
|||
|
||||
Running the App you should be able to select any files on your system.
|
||||
|
||||
== Solution 2 : Write your own Interaction-class
|
||||
|
||||
If you don't want to use Reactive-UI you can also write your own `Interaction`-class to provide a similar functionality. In this section you will see one possible solution.
|
||||
|
||||
NOTE: We are going to use the https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/[[CommunityToolkit.Mvvm\]] in this sample, especially their source generators. If you are not familiar with it yet, read the online docs they provide first.
|
||||
|
||||
=== Step1: Create the Interaction-class
|
||||
|
||||
In our project (or in a class library we use) we add a folder called `Core`. Inside this folder we add a new generic class called `Interaction`, which has basically the below mentioned API.
|
||||
|
||||
The class will have two generic parameters:
|
||||
|
||||
TInput:: The type of the input we expect
|
||||
TOutput:: The type of the output we expect
|
||||
|
||||
It will implement two interfaces:
|
||||
|
||||
ICommand:: This interface helps us to use the interaction like any other command
|
||||
IDisposable:: This interface helps us to unregister from event listeners
|
||||
|
||||
In addition we will add two methods:
|
||||
|
||||
IDisposable RegisterHandler(Func<TInput, Task<TOutput>> handler):: This method will be used by the View to register the action to be performed.
|
||||
|
||||
Task<TOutput> HandleAsync(TInput input):: This method will be called from the `ViewModel` with a given input and the `View` will return the requested output.
|
||||
|
||||
And this is how the final class looks like:
|
||||
|
||||
[source,c#]
|
||||
----
|
||||
/// <summary>
|
||||
/// Simple implementation of Interaction pattern from ReactiveUI framework.
|
||||
/// https://www.reactiveui.net/docs/handbook/interactions/
|
||||
/// </summary>
|
||||
public sealed class Interaction<TInput, TOutput> : IDisposable, ICommand
|
||||
{
|
||||
// this is a reference to the registered interaction handler.
|
||||
private Func<TInput, Task<TOutput>>? _handler;
|
||||
|
||||
/// <summary>
|
||||
/// Performs the requested interaction <see langword="async"/>. Returns the result provided by the View
|
||||
/// </summary>
|
||||
/// <param name="input">The input parameter</param>
|
||||
/// <returns>The result of the interaction</returns>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
public Task<TOutput> HandleAsync(TInput input)
|
||||
{
|
||||
if (_handler is null)
|
||||
{
|
||||
throw new InvalidOperationException("Handler wasn't registered");
|
||||
}
|
||||
|
||||
return _handler(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a handler to our Interaction
|
||||
/// </summary>
|
||||
/// <param name="handler">the handler to register</param>
|
||||
/// <returns>a disposable object to clean up memory if not in use anymore/></returns>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
public IDisposable RegisterHandler(Func<TInput, Task<TOutput>> handler)
|
||||
{
|
||||
if (_handler is not null)
|
||||
{
|
||||
throw new InvalidOperationException("Handler was already registered");
|
||||
}
|
||||
|
||||
_handler = handler;
|
||||
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
return this;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_handler = null;
|
||||
}
|
||||
|
||||
public bool CanExecute(object? parameter) => _handler is not null;
|
||||
|
||||
public void Execute(object? parameter) => HandleAsync((TInput?)parameter!);
|
||||
|
||||
public event EventHandler? CanExecuteChanged;
|
||||
}
|
||||
----
|
||||
|
||||
=== Step 2: Prepare the ViewModel
|
||||
|
||||
In our `CustomInteractionViewModel` we need to add a new instance of the `Interaction`. In our sample we want to provide a dialog title (`string`) as the input and we expect a list of selected files (`string[]?`)
|
||||
|
||||
[soruce,c#]
|
||||
----
|
||||
/// <summary>
|
||||
/// Gets an instance of our own Interaction class
|
||||
/// </summary>
|
||||
public Interaction<string, string[]?> SelectFilesInteraction { get; } = new Interaction<string, string[]?>();
|
||||
----
|
||||
|
||||
In a next step we add a Command which will call the interaction:
|
||||
|
||||
[soruce,c#]
|
||||
----
|
||||
[RelayCommand]
|
||||
private async Task SelectFilesAsync()
|
||||
{
|
||||
SelectedFiles = await SelectFilesInteraction.HandleAsync("Hello from Avalonia");
|
||||
}
|
||||
----
|
||||
|
||||
=== Step 3: Prepare the View
|
||||
|
||||
Somehow we need to register the `View` to the `Interaction` of the `ViewModel`. In Avalonia we have an event called `OnDataContextChanged` which we can listen to, or, if we are in code behind, simply override it.
|
||||
|
||||
[soruce, c#]
|
||||
----
|
||||
// Stores a reference to the disposable in order to clean it up if needed
|
||||
IDisposable? _selectFilesInteractionDisposable;
|
||||
|
||||
protected override void OnDataContextChanged(EventArgs e)
|
||||
{
|
||||
// Dispose any old handler
|
||||
_selectFilesInteractionDisposable?.Dispose();
|
||||
|
||||
if (DataContext is CustomInteractionViewModel vm)
|
||||
{
|
||||
// register the interaction handler
|
||||
_selectFilesInteractionDisposable =
|
||||
vm.SelectFilesInteraction.RegisterHandler(InteractionHandler);
|
||||
}
|
||||
|
||||
base.OnDataContextChanged(e);
|
||||
}
|
||||
----
|
||||
|
||||
WARNING: Remember that the DataContext can change several times. In order to not get any memory leak, we have to dispose any earlier registration to an older view model
|
||||
|
||||
The interaction handler itself is quite simple
|
||||
|
||||
[soruce,c#]
|
||||
----
|
||||
private async Task<string[]?> InteractionHandler(string input)
|
||||
{
|
||||
// Get a reference to our TopLevel (in our case the parent Window)
|
||||
var topLevel = TopLevel.GetTopLevel(this);
|
||||
|
||||
// Try to get the files
|
||||
var storageFiles = await topLevel!.StorageProvider.OpenFilePickerAsync(
|
||||
new FilePickerOpenOptions()
|
||||
{
|
||||
AllowMultiple = true,
|
||||
Title = input
|
||||
});
|
||||
|
||||
// Transform the files as needed and return them. If no file was selected, null will be returned
|
||||
return storageFiles?.Select(x => x.Name)?.ToArray();
|
||||
}
|
||||
----
|
||||
|
||||
=== Step 4: See it in action
|
||||
|
||||
Run the App and try to select as many files as you like.
|
||||
|
||||
== Related
|
||||
|
||||
|
@ -139,7 +300,6 @@ There are more ways to show dialogs from the ViewModel, for example:
|
|||
|
||||
* link:../AdvancedMvvmDialogSample[Dialog Service]
|
||||
* https://github.com/AvaloniaCommunity/awesome-avalonia#mvvm--mvp--mvu[third party libs]
|
||||
// Any related information or further readings goes here.
|
||||
|
||||
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче