Fully Cross platform REAL WORLD example with DI, Routing, AppSettings, Corporate design and Deployment
sandreas opened this issue · 7 comments
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.
- Initializing an AvaloniaUI Cross Platform App (including WASM)
- Integrating the
MainViewModel
in theMainWindow
for desktop apps- Assumed that I would integrate the
SingleView
into theMainWindow
, how would I do that?
- Assumed that I would integrate the
- Properly Integrate Dependency Injection / IoC (?)
- Avalonia is using
Splat
, right? So how would I inject the dependencies in aViewModel
in an elegant way? - I used
Microsoft.DependencyInjection.Extensions
- is that the right way to do it?
- Avalonia is using
- Proper Routing (?)
- Is
ReactiveUI
the only way Routing could work? (seeCommunityToolkit.Mvvm
below) - I implemented a small
RoutingService
to instanciate ViewModels and navigate between them, but is this PROPER Routing already?
- Is
- Storing and loading Cross Platform AppSettings (e.g. a Selected Theme, Api credentials, etc.)
- How would I do that?
- Implementing a corporate design (Replacing Icons, the wasm Powered by Avalonia initializer, etc. with custom ones)
- I already replaced some Icons, but I would like to give the app a look and feel that does not have any missing or fallback graphics. I would have to make a list, which files and parts of code to replace
- Deployment (for every Platform)
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.
- Unit-Testing
- Using CommunityToolkit.Mvvm instead of ReactiveUI (incl. Routing)
- Using command line options to configure / initialize something (e.g.
myapp file.txt
opensfile.txt
in the editor) - Keyboard Shortcuts (for desktop only)
- Native Menus (e.g. About)
- Bundling / Creating an Installer
- Handling uncaught Exceptions
- Running Background Tasks
- Multilanguage support
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.
- I'll send a pull request with a proposal for a sample (as long as I get help with my open questions and suggestions how to do things the right way)
Cross platform TodoApp:
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.
Creating a new cross platform project
dotnet new avalonia.xplat -o TodoApp
Replace ReactiveUI with CommunityToolkit
- Remove
Avalonia.ReactiveUI
- Install
CommunityToolkit.Mvvm
(only to theTodoApp
project) - Remove all traces of
ReactiveUI
from the projectTodoApp.Desktop/Program.cs
: RemoveReactiveUI
TodoApp/ViewModels/ViewModelBase.cs
: ReplaceReactiveObject
withObservableObject
TodoApp/ViewModels/MainViewModel.cs
: Make it apartial class
to be able to use code generators
Dependency Injection / IoC
To 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();
}
}
Create first ViewModel
dotnet new avalonia.usercontrol -o Views -n TodoListView --namespace Todo.Views
Follow along with the old Todo tutorial.
App Design
Probably you would like to have your app a custom design (e.g. icons).
Replacing the app icon
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
Using svg
images
AvaloniaUI does not support svg
rendering by default, but there is a library called Svg.Skia
for this.
Building a Release
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
:
- First install nuget
NSubsys
1.0 (the project is archived, but it still works) - Add the lines below to your
Todo.Dekstop/TodoApp.csproj
(ensure the path toNSubsys.Tasks.dll
is correct) - Run the build
<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.
@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 asavalonia.xplat
(on Linux with Rider)CommunityToolkit.Mvvm
instead ofReactiveUI
(I prefer the code generation via attributes like[ObservableProperty]
and[RelayCommand]
)Microsoft.DependencyInjection.Extensions
as IoC Container- A small
RoutingService
(NavigationService
) to switch between view models in theMainView
- Use
svg
icons withSvg.Skia
- Build a custom multisize App
ico
fromsvg
withconvert
- Small shell script to deploy for most platforms (except macOS / iOS of course, this will later be a github action)
- Remove the annoying command line window in the background by using
NSubsys
patching (it's still there)
- Remove the annoying command line window in the background by using
What I did not achieve / try out so far:
- Multiplatform App settings (I think I'll implement a
SettingsService
abstracting a combination of<UseMauiEssentials>true</UseMauiEssentials>
for mobile platforms andMicrosoft.Extensions.Configuration
for everything else - Replacing all icons and WASM to contain a Corporate Design without Avalonia traces
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.)
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.
- https://github.com/sandreas/Avalonia.SimpleRouter - a routing library, works pretty well for my use cases
- https://github.com/sandreas/Avalonia.Preferences - a Preferences storage - this is very rough and does not support secure storage, here I would definitely prefer to use Avalonia.Essentials, as soon as there is a release.
However, there still is
- https://github.com/AvaloniaUI/Avalonia.Essentials, which should integrate
Preferences
andSecureStorage
, but neither I did not find a release nor know it's build status - https://github.com/OutSystems/CefGlue, which provides a WebView, but in my case was not stable enough
Hello,
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:
- View and the Codes-behind (such as:
MainView.axaml
&MainView.axaml.cs
) are considered not testable (1) - But MVs (such as
MainViewModel.cs
) are testable. - In the "MV-first" approach, you have to stays (far) away from the ("non-testable") codes behind (
MainView.axaml.cs
). You (or your designer) should work withMainView.axaml
and You work (and wirte tests for) theMainViewModel.cs
. - In the "MV-first" approach, you create thes ViewModels (with help of your DI Container) and handle interraction between them.
- The
WalletVM
might contain otherCardVM
=> makeWalletVM
s depends onCardVM
- A
WalletVM
might also send events to other VMs via a meditor.
- The
This project is my playground which followed the above "MV-first" principles:
- I used the Microsoft DI framework to resolve my View, my ViewModel, everything..
- I can unit-test my VMs,
=> 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.