Open sandreas opened 1 year ago
In this tutorial we're going to be creating a more sophisticated cross platform TODO application in Avalonia using the Model-View-ViewModel (MVVM) pattern together with some more advanced principles like Dependency Injection, Routing, AppSettings, Corporate Design and a full deployment on all platforms.
dotnet new avalonia.xplat -o TodoApp
Avalonia.ReactiveUI
CommunityToolkit.Mvvm
(only to the TodoApp
project)ReactiveUI
from the project
TodoApp.Desktop/Program.cs
: Remove ReactiveUI
TodoApp/ViewModels/ViewModelBase.cs
: Replace ReactiveObject
with ObservableObject
TodoApp/ViewModels/MainViewModel.cs
: Make it a partial class
to be able to use code generatorsTo use the de facto C# standard DI container, add Microsoft.DependencyInjection.Extensions
as dependency.
Then edit the TodoApp/App.xaml.cs
to contain the following:
using System;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Microsoft.Extensions.DependencyInjection;
using TodoApp.ViewModels;
using TodoApp.Views;
namespace TodoApp;
public partial class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
IServiceProvider services = ConfigureServices();
var mainViewModel = services.GetRequiredService<MainViewModel>();
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow
{
DataContext = mainViewModel
};
}
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
{
singleViewPlatform.MainView = new MainView
{
DataContext = mainViewModel
};
}
base.OnFrameworkInitializationCompleted();
}
private static ServiceProvider ConfigureServices()
{
var services = new ServiceCollection();
services.AddSingleton<MainViewModel>();
//services.AddTransient<SecondaryViewModel>();
return services.BuildServiceProvider();
}
}
dotnet new avalonia.usercontrol -o Views -n TodoListView --namespace Todo.Views
Follow along with the old Todo tutorial.
Probably you would like to have your app a custom design (e.g. icons).
Usually, you would use svg
to build icons to make them scalable for every purpose. However, by default on windows App icons are stored in ico
format, so if you would like to build a multi sized icon, you could use ImageMagick
(the command line tool convert
):
# create temporary icons in your relevant sizes
convert icon.svg -scale 16 16.png
convert icon.svg -scale 32 32.png
convert icon.svg -scale 48 48.png
convert icon.svg -scale 64 64.png
convert icon.svg -scale 128 128.png
convert icon.svg -scale 256 256.png
# create multisize icon
# convert 16.png 32.png 48.png 64.png 128.png 256.png icon.ico
convert *.png icon.ico
svg
imagesAvaloniaUI does not support svg
rendering by default, but there is a library called Svg.Skia
for this.
Here are some command line instructions to build different platforms (incomplete):
# wasm
dotnet publish TodoApp.Browser -c Release -o dist/wasm/
# android pkg
dotnet publish TodoApp.Android -f net7.0-android -c Release -o dist/android/
# linux, windows (x64)
dotnet publish TodoApp.Desktop -f net7.0 -r "win-x64" -c "Release" -o dist/win-x64
dotnet publish TodoApp.Desktop -f net7.0 -r "linux-x64" -c "Release" -o dist/linux-x64
On windows you might have an annoying commandline window in the background when using specific build flags, e.g. -p:PublishSingleFile=true --self-contained true -p:PublishReadyToRun=true
. To remove that, you can use NSubsys
:
NSubsys
1.0 (the project is archived, but it still works)Todo.Dekstop/TodoApp.csproj
(ensure the path to NSubsys.Tasks.dll
is correct)<Project Sdk="Microsoft.NET.Sdk">
<!-- ... -->
<PropertyGroup>
<NSubsysTasksPath Condition="'$(NSubsysTasksPath)' == ''">$(HOME)/.nuget/packages/nsubsys/1.0.0/tool/NSubsys.Tasks.dll</NSubsysTasksPath>
</PropertyGroup>
<UsingTask TaskName="NSubsys.Tasks.NSubsys" AssemblyFile="$(NSubsysTasksPath)" />
<Target Name="CustomAfterBuild" AfterTargets="Build" Condition="$(RuntimeIdentifier.StartsWith('win'))">
<NSubsys TargetFile="$(OutputPath)$(AssemblyName)$(_NativeExecutableExtension)" />
</Target>
<Target Name="CustomAfterPublish" AfterTargets="Publish" Condition="$(RuntimeIdentifier.StartsWith('win'))">
<NSubsys TargetFile="$(PublishDir)$(AssemblyName)$(_NativeExecutableExtension)" />
</Target>
</Project>
@sandreas as you want to make the sample fully xplat, I would prefer to wait until Avalonia 11.0 is out. The reason is that 11.0 is much better when it comes to xplat solutions and honestly I don't want to rely on previews as there may be confusing breaking changes.
@timunie
@sandreas as you want to make the sample fully xplat, I would prefer to wait until Avalonia 11.0 is out.
Sure. I'm using the preview anyway for my app because the timeframe getting ready is not set to a deadline - it's planned as an Open Source project, as soon I've ironed out the little annoying things.
I don't want to rely on previews as there may be confusing breaking changes.
As you wish. I'm going to document the stuff I found out here, if you don't mind, so feel free to take it as a base for a new tutorial and if you would like to get access to my private experimental repository, you just have to ask.
What I achieved so far for my testing TodoApp:
Avalonia 11 preview 5
project as avalonia.xplat
(on Linux with Rider)CommunityToolkit.Mvvm
instead of ReactiveUI
(I prefer the code generation via attributes like [ObservableProperty]
and [RelayCommand]
)Microsoft.DependencyInjection.Extensions
as IoC ContainerRoutingService
(NavigationService
) to switch between view models in the MainView
svg
icons with Svg.Skia
ico
from svg
with convert
NSubsys
patching (it's still there)What I did not achieve / try out so far:
SettingsService
abstracting a combination of <UseMauiEssentials>true</UseMauiEssentials>
for mobile platforms and Microsoft.Extensions.Configuration
for everything elseThere where some other annoying little things, that I'm going to report issues on the main project (not that you are already have enough) - e.g. binding a Command
in ItemsControl
did not work in any way I know of (Style, Relative binding path, etc.)
There where some other annoying little things, that I'm going to report issues on the main project (not that you are already have enough) - e.g. binding a Command in ItemsControl did not work in any way I know of (Style, Relative binding path, etc.)
Issues should only reported trying latest nightly if possible. Also you can ping me on telegram in our Avalonia chat. I'd like to help you iron out issues you have, as long as I know how to do.
@sandreas, I stumbled upon this issue and now that Avalonia 11 is out, I was wondering where this example that you are working on may exist. Thanks!
I stumbled upon this issue and now that Avalonia 11 is out, I was wondering where this example that you are working on may exist. Thanks!
@fgperry There is no documented Example, but currently I am working on upgrading ToneAudioPlayer from Avalonia 11 preview 6
to stable Avalonia 11
. The code should already mostly apply to Avalonia 11 stable. I'm also working on a little blog series including development, deployment on all platforms, caveats and possible improvements of ToneAudioPlayer, but as always, my time is just too limited.
I also worked on a bigger customer project with Avalonia, but I had to switch to MAUI (which I really was not happy with - I think MAUI is so much worse), because I could not get working the WebView
and SecureStorage
at the time. This unfortunately was a K.O.
I think this is one of Avalonia's only weaknesses (if there is ANY), that the default libraries for daily tasks are not that mature. While I did not find implementations / libraries for WebView
, SecureStorage
, Preferences
, Routing
and , I released my own libs for the latter.
However, there still is
Preferences
and SecureStorage
, but neither I did not find a release nor know it's build statusHello,
Disclaimer: I'm discovering avalonia as well.
After research and experimenting things, I think that this small post shows the right way to make Big complex Avalonia applications testable and maintainable, and is the right way Dependency Injection should be used in an Avalonia application.
The main keyword in the post is MV-first:
MainView.axaml
& MainView.axaml.cs
) are considered not testable (1)MainViewModel.cs
) are testable.MainView.axaml.cs
). You (or your designer) should work with MainView.axaml
and You work (and wirte tests for) the MainViewModel.cs
.WalletVM
might contain other CardVM
=> make WalletVM
s depends on CardVM
WalletVM
might also send events to other VMs via a meditor.This project is my playground which followed the above "MV-first" principles:
=> I'm confident that this is the right way for Big, complex Avalonia app
(1): Views are testable, there are tools, lib helping us.. but their tests are usually hard to setup, expensive to maintain and to evole => Just consider them as not testable to make things simple.
Hey there,
I'm missing a sample for developing a REAL WORLD App with all the bells and whistles you would like to have in a "professional" App (I'm trying to adopt Avalonia for a company app). The Todo and Music Store App examples are great, but still kind of missing a real world scenario using features you probably would like to have in such a project that should also be deployed on mobile platforms. Even AngelSix, who I think you hired to do an Avalonia Series and who is doing a great job on this, is currently still missing some of these topics that are elementary for a non-beginner.
I would provide a sample myself but I'm pretty new to Avalonia and struggling a bit with these topics (not in general, I got it working, but how to do it the RIGHT way)... At least I'm willing to submit a pull request for a sample Todo app incl. tutorial, but I would need your help to cover the topics below (links or short code examples would be awesome).
Topics to cover
I've only fully completed the checked parts and have a lot of questions about the unchecked ones.
MainViewModel
in theMainWindow
for desktop appsSingleView
into theMainWindow
, how would I do that?Splat
, right? So how would I inject the dependencies in aViewModel
in an elegant way?Microsoft.DependencyInjection.Extensions
- is that the right way to do it?ReactiveUI
the only way Routing could work? (seeCommunityToolkit.Mvvm
below)RoutingService
to instanciate ViewModels and navigate between them, but is this PROPER Routing already?Extended Topics (nice to have)
Here are some topics that would be nice to have later, but that I would not integrate in the first place to keep things simple.
myapp file.txt
opensfile.txt
in the editor)Ideas for a sample
I think Camelot is a pretty good example for many of these topics, but unfortunately not fully cross platform (Android, iOS and WASM are missing) and if you would like to create an App for Android or iOS, a dual pane file manager is not the right choice in my opinion.
So I would extend / add the topics to the existing Todo List tutorial. That would make it easier to keep up to date and to follow along. Although Todo List is pretty simple, it could add some features and need routing, use DI, store some settings for an API URL, etc.