Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fully Cross platform REAL WORLD example with DI, Routing, AppSettings, Corporate design and Deployment #43

Open
5 of 8 tasks
sandreas opened this issue Mar 11, 2023 · 7 comments
Labels
enhancement New feature or request

Comments

@sandreas
Copy link

sandreas commented Mar 11, 2023

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 the MainWindow for desktop apps
    • Assumed that I would integrate the SingleView into the MainWindow, how would I do that?
  • Properly Integrate Dependency Injection / IoC (?)
    • Avalonia is using Splat, right? So how would I inject the dependencies in a ViewModel in an elegant way?
    • I used Microsoft.DependencyInjection.Extensions - is that the right way to do it?
  • Proper Routing (?)
    • Is ReactiveUI the only way Routing could work? (see CommunityToolkit.Mvvm below)
    • I implemented a small RoutingService to instanciate ViewModels and navigate between them, but is this PROPER Routing already?
  • 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 opens file.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)
@sandreas sandreas added the enhancement New feature or request label Mar 11, 2023
@sandreas
Copy link
Author

sandreas commented Mar 12, 2023

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 the TodoApp project)
  • Remove all traces of 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 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 to NSubsys.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>

@timunie
Copy link
Collaborator

timunie commented Mar 13, 2023

@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
Copy link
Author

sandreas commented Mar 15, 2023

@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 Container
  • A small RoutingService (NavigationService) to switch between view models in the MainView
  • Use svg icons with Svg.Skia
  • Build a custom multisize App ico from svg with convert
  • Small shell script to deploy for most platforms (except macOS / iOS of course, this will later be a github action)

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 and Microsoft.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.)

@timunie
Copy link
Collaborator

timunie commented Mar 15, 2023

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.

@fgperry
Copy link

fgperry commented Aug 23, 2023

@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!

@sandreas
Copy link
Author

sandreas commented Aug 24, 2023

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

@duongphuhiep
Copy link

duongphuhiep commented Aug 30, 2024

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 with MainView.axaml and You work (and wirte tests for) the MainViewModel.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 other CardVM => make WalletVMs depends on CardVM
    • A WalletVM might also send events to other VMs via a meditor.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

4 participants