diff --git a/_docs/avalonia-docs-theme.yml b/_docs/avalonia-docs-theme.yml index 0d189dd1..fc1a82d5 100644 --- a/_docs/avalonia-docs-theme.yml +++ b/_docs/avalonia-docs-theme.yml @@ -1,4 +1,4 @@ -extends: default +extends: default-with-font-fallbacks base: font-color: #222222 background-color: #ffffff diff --git a/scripts/Ascii-doc-pdf_creator.ps1 b/scripts/Ascii-doc-pdf_creator.ps1 index 2f2f115c..049a1997 100644 --- a/scripts/Ascii-doc-pdf_creator.ps1 +++ b/scripts/Ascii-doc-pdf_creator.ps1 @@ -19,5 +19,5 @@ foreach ($file in $adocFiles) { $pdfPath = [System.IO.Path]::ChangeExtension($file.FullName, ".pdf") Write-Host "Compiling $($file.FullName) to $pdfPath" - asciidoctor-pdf "$($file.FullName)" -r asciidoctor-diagram -a pdf-theme="$themePath" -a allow-uri-read=true -a source-highlighter=rouge -o "$pdfPath" --trace + asciidoctor-pdf "$($file.FullName)" -r asciidoctor-diagram -a pdf-theme="$themePath" -a allow-uri-read=true -a source-highlighter=rouge -a compress=screen -o "$pdfPath" --trace } \ No newline at end of file diff --git a/src/Avalonia.Samples/CompleteApps/Avalonia.MusicStore/README.adoc b/src/Avalonia.Samples/CompleteApps/Avalonia.MusicStore/README.adoc index 463449b3..a3470c54 100644 --- a/src/Avalonia.Samples/CompleteApps/Avalonia.MusicStore/README.adoc +++ b/src/Avalonia.Samples/CompleteApps/Avalonia.MusicStore/README.adoc @@ -1,5 +1,6 @@ = Music Store App // --- D O N ' T T O U C H T H I S S E C T I O N --- +ifdef::env-github[] :toc: :toc-placement!: :tip-caption: :bulb: @@ -7,26 +8,36 @@ :important-caption: :heavy_exclamation_mark: :caution-caption: :fire: :warning-caption: :warning: +endif::[] + +ifndef::env-github[] +:icons: font +:icon-set: fas +endif::[] // ---------------------------------------------------------- // Write a short summary here what this examples does -This example will show you how to create a simple music store using Avalonia and MVVM community toolkit. You'll learn the MVVM pattern with the MVVM community toolkit to manage multiple application windows. Also you will use advanced asynchronous techniques to implement the album search and other features, so that application responsiveness is maintained. +In this tutorial you will create a desktop app based on the idea of a music store. The app is highly graphical - it presents images of album covers, and uses semi-transparent 'acrylic' blurred window backgrounds to give a very up-to-date look. By the end of the tutorial, you will be able search the iTunes online list of albums, and select albums for your own list. + +[[final_result,finished app]] +.This is how the final App should look like +image::_docs/initial_preview.png[This is how the final App should look like] // --- D O N ' T T O U C H T H I S S E C T I O N --- toc::[] // --------------------------------------------------------- - +[discrete] === Difficulty // Choose one of the below difficulties. You can just delete the ones you don't need. đŸ„ Easy đŸ„ - +[discrete] === Buzz-Words // Write some buzz-words here. You can separate them by ", " @@ -35,7 +46,12 @@ Music Store, Complete App, CommunityToolkit.MVVM, Mvvm.Messaging, Styles, Observ == Before we start -This is a more advanced tutorial. The 'To Do List App' is a recommended prerequisite if you have limited experience with the MVVM pattern. Find 'To Do List App' tutorial link:../../CompleteApps/SimpleToDoList[here]. + +In this tutorial you will learn how to use the MVVM pattern with the https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/[[MVVM community toolkit\]] to manage multiple application windows. Also you will use advanced asynchronous techniques to implement the album search and other features, so that application responsiveness is maintained. + +TIP: This is a more advanced tutorial. The 'To Do List App' is a recommended prerequisite if you have limited experience with the MVVM pattern. Read about the 'To Do List App' tutorial link:../../CompleteApps/SimpleToDoList[[here\]]. + +NOTE: For information and background on the concept of the MVVM pattern, see https://docs.avaloniaui.net/docs/concepts/the-mvvm-pattern/[[here\]]. This sample assumes that you have a basic knowledge about the following topics: @@ -47,18 +63,1824 @@ This sample assumes that you have a basic knowledge about the following topics: TIP: Some sections are optional. You can skip these if you want to. === MVVM pattern -For information and background on the concept of the MVVM pattern, refer to the official documentation link:https://docs.avaloniaui.net/docs/concepts/the-mvvm-pattern/[here]. +For information and background on the concept of the MVVM pattern, refer to the official documentation link:https://docs.avaloniaui.net/docs/concepts/the-mvvm-pattern/[[here\]]. +=== Messengers +This tutorial uses the messaging features of the CommunityToolkit.Mvvm to manage communication between view models and views. This is one approach to share data between different classes, without "knowing each other". If you are not familiar with it, you can read about it in the official documentation link:https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/messenger[[here\]]. == The Solution -You can follow the complete step-by-step guide for building the Music Store App in the official tutorial link:https://docs.avaloniaui.net/docs/tutorials/music-store-app/[here]. -== Related +=== 1, Create a New Project + +Before you can start programming, you will have to create a new project for the app. Please see the https://docs.avaloniaui.net/docs/get-started/[[getting started guide\]] if you need help installing Avalonia and setting up your IDE. + +Most IDEs have templates for creating new Avalonia projects. In this tutorial you will use the 'Avalonia MVVM Application' template, which is available in both _Visual Studio_ and _JetBrains Rider_. Make sure you have selected the _CommunityToolkit.Mvvm_ option when creating the project. + +TIP: If you have _ReactiveUI_ installed, you can uninstall this and install _CommunityToolkit.Mvvm_ instead. Make sure to also update your usages. + +A new project will be created with the following solution folders and files: + +.The basic structure of the created project +image::_docs/2_rider_proj_structure.png[This is the structure of the creates project] + +NOTE: The file names may differ a little. For example "MainViewModel.cs" may be named "MainViewModel.cs" depending on the IDE you are using. + +==== Update Project Dependencies if needed + +Let's make sure you use correct version of CommunityToolkit.Mvvm: + + - Locate project file Avalonia.MusicStore.csproj + - Right-click on the project name in the Solution Explorer. + - Select 'Edit' -> 'Edit Avalonia.MusicStore.csproj' + +[[prepare-project-for-partial-properties, Setup the project]] +In the opened .csproj file, ensure you have the correct CommunityToolkit.Mvvm package version no older than 8.4.0 and +Avalonia version no older than 11.3.0. + +.Update nuget packages +```xml + + + + +``` +In the same file enable preview C# language features: + +.Within the _PropertyGroup_ section, add the following line: +```xml +preview +``` + +This setting enables support for the latest C# features required by this tutorial, including partial properties introduced in C# 13. + +Now take some time to review the files and folders that the solution template created. You will see that the following the MVVM pattern, these folders were created: + +[cols="20h,~"] +|=== +| Folder Name |Description + +|Assets +|Contains any embedded assets that are compiled into the program. `Images`, `Icons`, `Fonts` etc, anything that the UI +Folder Name Description +might need to display, + +|Models +|This is an empty folder for code that is the 'model' part of the MVVM pattern. This often contains everything else the app needs that is not part of the UI. For example: interaction with a database, Web API, or interfaces with a hardware device. + +|View Models +|This is a folder for all the view models in the project, and it already contains an example. View models contain the application logic in the MVVM pattern. For example: a button is enabled only when the user has typed something; or open a dialog when the user clicks here; or show an error if the user enters too high a number type of logic in this input. + +|Views +|This is a folder for all the views in the project, and it already contains the view for the application main window. Views in the MVVM pattern contain only the presentation for the application; that is layout and form, fonts, colors, icons and images. In MVVM they have only enough code to link them to the view model layer. In _Avalonia UI_ there is only enough code to manage windows and dialogs here. +|=== + + +NOTE: To explore the concepts behind the MVVM pattern, and when is appropriate to use it, see https://docs.avaloniaui.net/docs/concepts/the-mvvm-pattern/[[Avalonia-docs\]] + +The solution template has created enough files for the application to run. You will meet all of these during the rest of this tutorial. + +==== Run the Project + +Press the debug button in your IDE to compile and run the project. + +This will show a window that looks like: + +.first run of the created project +image::_docs/5_first_run.png[First run] + +It is a little plain - but you now have a running application, and a blank canvas to start developing with. + +=== Window Styling + +Now, you will make the main window look modern by applying a dark theme, and an acrylic blur to the window background. + +==== Dark Mode + +Follow this procedure to style the main window in 'dark' mode: + +- Stop the app if it is still running. +- Locate and open the file **App.axaml**. +- In the XAML, change the `RequestedThemeVariant` attribute in the `` element from "Default" to "Dark" ++ +```xml + +``` + +- Now locate and open the **MainWindow.axaml** file in the **/Views** folder. ++ +NOTE: Notice that the preview pane is still showing the window in 'light' mode. The application will require a rebuild for the new mode to show in the preview pane. + +- Click **Build Startup Project** on the **Build** menu. ++ +The preview pane now changes to the dark mode. ++ +image:_docs/6_DarkMode.png[Previewer showing the dark mode] + +==== Acrylic Blur + +Follow this procedure to style the background of the main window with an acrylic blur: + +- Locate and open the **MainWindow.axaml** file in the **/Views** folder. +- Find the end of the opening tag of the `` element. +- After the `Title="Avalonia.MusicStore"` attribute, add two new attributes as follows: ++ +```xml + +``` + +- To apply the acrylic effect to the whole window, replace the `` element in the content zone of the main window with the following XAML for a panel: ++ +```xml + + + + + + + + + +``` + +- Click **Debug** to compile and run the project. ++ +.Acrylic materia applied +image::_docs/7_AcrylicBlur.png[Acrylic materia applied] + + +Notice that, as expected, the acrylic window effect covers the content zone of the main window. However the effect does not yet extend to the title bar. + +WARNING: Note that _Linux_ users can not yet take advantage of the following code due to limitations of the X11 version. The tutorial code will run and the window will still work on _Linux_, but the full effect will not be realized. + +Follow this procedure to extend the acrylic blur effect onto the title bar: + +- Stop the app if is still running. +- Find the end of the opening tag of the `` element again. +- Add the `ExtendClientAreaToDecorationsHint` attribute as shown: ++ +```xml + +``` + +- Click **Debug** to compile and run the project. + +.Fully acrylic window +image::_docs/8_FullAcrylicWindow.png[Fully acrylic window] + +Now you have the acrylic blur effect extending into the title bar. + + +=== Add and Layout Controls + +The main window of the app will eventually show a list of album covers in the user's collection, with a button at its top-right corner to allow the user to add a new album. The button will open a search dialog window to find new albums to add. + +On this page you will learn how to layout the main window so that the button appears at its top-right corner, as required. + +==== Button Layout + +To display a button in the content zone of the main window, follow this procedure: + +- Stop the app if it is still running. +- Locate and open the **MainWindow.axaml** file. +- Inside the panel element, add the following XAML for a button. The panel XAML should look like this: ++ +```xml + + + + + + + + +``` + +- Click **Debug** to compile and run the project. ++ +.The button now has an icon +image::_docs/10_Button_with_icon.png[Button with icon] + +=== Button Command + +So far in this tutorial, you have altered only files from the view part of the MVVM pattern (for the main window and app). In this section you will learn how to link the button in the view for the main window, to a command in the view model. This will cause user interaction with the view (in this case a button click) to have an effect in the application logic of the view model. + +When you develop with _Avalonia UI_ and the MVVM pattern, the solution template will give you a choice of MVVM toolkits. This tutorial now uses _CommunityToolkit.Mvvm_, and the solution template has already added the necessary packages. + +==== RelayCommand + +The first step in linking the view and view model is to make the view model able to accept a command. You will achieve this by adding a method to the main window view model and decorating it with the `[RelayCommand]` attribute, which will generate a bindable `ICommand` property, which can be referenced from your view. +Follow this procedure: + +- Stop the app if it is still running. +- Locate and open the **MainViewModel.cs** file in the **/ViewModels** folder. +- Delete the existing content of the class, and add the code shown: ++ +```csharp +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using System.Threading.Tasks; + +namespace Avalonia.MusicStore.ViewModels +{ + public partial class MainViewModel : ObservableObject + { + public MainViewModel() + { + // ViewModel initialization logic. + } + + [RelayCommand] + private async Task AddAlbumAsync() + { + // Code here will be executed when the button is clicked. + } + } +} +``` + +==== How it works +The `[RelayCommand]` attribute generates a public property for you at compile time named `AddAlbumCommand`, which implements `ICommand`. + +This means that even though you only wrote a method named `AddAlbumAsync`, Avalonia's data-binding system can bind directly to `AddAlbumCommand` in your AXAML — without you writing any boilerplate command logic. + +TIP: If you want to see how this method is executes, you can place a debug breakpoint at the opening curly brace inside the `AddAlbumAsync()` method. + +To complete the link from the view to your new `AddAlbumAsync` view model property, you will add a data binding to the button. + +NOTE: For more information about the concept of data binding, see https://docs.avaloniaui.net/docs/basics/data/data-binding[[here\]]. + +To add the button data binding, follow this procedure: + +- Locate and open the **MainWindow.axaml** file. +- Find the XAML for the button and add the command attribute and binding, as shown: ++ +```xml + +``` + +==== Why it is `AddAlbumCommand`? +The `[RelayCommand]` attribute automatically generates command properties based on your method names. If your method name ends with _Async_, the generator removes the _Async_ suffix and appends _Command_ to form the property name. +If the method returns a Task, `[RelayCommand]` automatically generates an `IAsyncRelayCommand` instead of a regular `IRelayCommand`, giving you full support for asynchronous execution. +This means: +- If your method is named `AddAlbumAsync`, the generated property will be called `AddAlbumCommand`. +- If your method is named `AddAlbum`, it also becomes `AddAlbumCommand`. + +NOTE: Learn more about asynchronous `RelayCommand` generation in https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/generators/relaycommand#asynchronous-commands[[the official docs\]]. + +The `Command` property of an _Avalonia UI_ button determines what happens when the button is clicked. In this case it binds to the `AddAlbumCommand` generated in your view model, causing the `AddAlbumAsync` method to run. + +- Click **Debug** to compile and run the project. +- Click the icon button. + +You will see the app stop executing at the breakpoint you previously set in the view model. + + +=== Open a Dialog + +On this page you will learn how to open dialog window in your app and exchange data between windows using Mvvm.Messaging. The dialog will be used to search for and select an album to add to a list in the main window. + +Several messages will be used in your app: + +PurchaseAlbumMessage:: sent by the main view model to request the dialog window be shown and await a result. +MusicStoreClosedMessage:: sent by the dialog's view model when the user selects an album, to return the result and close the dialog. +CheckAlbumAlreadyExistsMessage:: sent by the dialog's view model before sending the `MusicStoreClosedMessage` to the main view model in order to make sure the album is not yet present. This part is optional +NotificationMessage:: sent by the main view model to display a notification, for example when an album was bought successfully. This part is optional. + +Below is a stripped down diagram showing the message flow between the components that you are going to implement in the next steps: + +```mermaid +graph TD; + A[MainViewModel] -->|Send PurchaseAlbumMessage| B(MainWindow) + B -->|Show MusicStoreWindow
await AlbumViewModel| C[MusicStoreWindow] + C -->|BuyMusic| D[MusicStoreViewModel] + D -->|Send MusicStoreClosedMessage
with SelectedAlbum| C + C -->|Close dialog
return SelectedAlbum| B + B -->|Reply with AlbumViewModel| A +``` +NOTE: The diagram above is simplified to show the basic message flow. In the actual implementation, there are additional message exchanges for checking if an album already exists and for displaying notifications. + +=== Add a New Dialog Window + +There is nothing special about a window view file that makes it into a dialog; that is up to the way in which the window is controlled by the app. You will use Avalonia UI features and _CommunityToolkit.Mvvm_ to manage this. So the first step is to create a new window for the app. + +To create a new window, follow this procedure: + +- Stop the app if it is still running. +- In the solution explorer, right-click the **/Views** folder and then click **Add**. +- Click **Avalonia Window**. +- When prompted for the name, type 'MusicStoreWindow' +- Press enter. + +==== Dialog Window Styling + +To style the new dialog window so that it matches the main window, follow the same procedure as explain in the section "<>" for the main window. + +==== Dialog Input and Output + +The application logic for the dialog will be controlled by its own view model. This will be created and linked to the dialog window view whenever the dialog is to be shown. + +Similarly, the result of the users interaction with the dialog will eventually have to be passed back to the application logic for the main window for processing. + +At this stage you will create two empty view model classes to act as placeholders for the dialog view model, and the dialog return (selected album) object. To create these view models, follow this procedure: + +- In the solution explorer, right-click the **/ViewModels** folder and then click **Add**. +- Click **Class**. +- Name the class 'MusicStoreViewModel' and click **Add**. +- Right-click again the **/ViewModels** folder and then click **Add** a second time. +- Click **Class**. +- Name the class 'AlbumViewModel' and click **Add**. + +=== Show Dialog + +Now that you have a new window `MusicStoreWindow` and the corresponding view models `MusicStoreViewModel` and `AlbumViewModel`. +You are going to complete the logic so that: + +* The main window view model sends a message requesting the dialog to be shown. +* The main window view receives that message, opens the dialog, and returns the result. + +Below is how this works step-by-step using the CommunityToolkit.Mvvm messaging API. + +==== Define the PurchaseAlbumMessage +- In the project root directory create new folder **/Messages** +- In the newly created **/Messages** folder add a class **PurchaseAlbumMessage**. + +First, you are going to define a message class called `PurchaseAlbumMessage` that carries an `AlbumViewModel` response. +This message will be sent by the view model when it needs to show the dialog. + +Open **PurchaseAlbumMessage.cs** and add the following code there: + +```csharp +using Avalonia.MusicStore.ViewModels; +using CommunityToolkit.Mvvm.Messaging.Messages; + +namespace Avalonia.MusicStore.Messages; + +public class PurchaseAlbumMessage : AsyncRequestMessage; +``` + +_`AsyncRequestMessage`_ lets you send a request and await a reply of type T (in our case, AlbumViewModel?). + +==== Register the Message Handler in MainWindow +In _MainWindow.axaml.cs_ register a handler for `PurchaseAlbumMessage`. This handler runs whenever the view model sends that message. Its job is to: + +- Create the dialog window. +- Assign `MusicStoreViewModel` as its DataContext. +- Call `ShowDialog` and pass the result back via m.Reply(...). + + +Open _MainWindow.axaml.cs_ and add the following code into MainWindow constructor: +```csharp +public MainWindow() +{ + InitializeComponent(); + + if (Design.IsDesignMode) + return; + + // Whenever 'Send(new PurchaseAlbumMessage())' is called, invoke this callback on the MainWindow instance: + WeakReferenceMessenger.Default.Register(this, static (w, m) => + { + // Create an instance of MusicStoreWindow and set MusicStoreViewModel as its DataContext. + var dialog = new MusicStoreWindow + { + DataContext = new MusicStoreViewModel() + }; + // Show dialog window and reply with returned AlbumViewModel or null when the dialog is closed. + m.Reply(dialog.ShowDialog(w)); + }); +} +``` + +==== Send the Message from the ViewModel +Now, update the `AddAlbumAsync()` method inside `MainViewModel` to send `PurchaseAlbumMessage` when the user clicks on the store button. +- Open **MainViewModel.cs** +- Locate the `AddAlbumAsync()` method that we added in the previous steps. +- Edit `AddAlbumAsync()` as shown: +```csharp +[RelayCommand] +private async Task AddAlbumAsync() +{ + // Send the message to the previously registered handler and await the selected album + var album = await WeakReferenceMessenger.Default.Send(new PurchaseAlbumMessage()); +} +``` +Now: +- Click **Debug** to compile and run the project. +- Click the icon button. + +It all works - but the dialog window opens at the same size as the main window, and offset from it. + +==== Dialog Position and Size + +For the final tweak, you will make the dialog smaller than the main window, and open centered on it. You will also make the main window open in the center of the user's screen. + +Follow this procedure: + +- Stop the app if it is still running. +- Locate and open the **MainWindow.axaml** file. +- Add an attribute to the `` element to set the start-up position: + +```xml + +``` + +- Locate and open the **MusicStoreWindow.axaml** file. +- Add attributes for the width and height of the dialog, set at 1000 and 550 respectively. +- Add the start-up position attribute set to `CenterOwner`, as shown: + +```xml + +``` + +- Click **Debug** to compile and run the project. +- Click the icon button. + +.Dialog opened centered +image::_docs/12_opened_dialog.png[dialog window open centered] + +The dialog window is now opened centered inside the main window. + + +=== Add Dialog Content + +Now you will learn how to add some content to the dialog window. This will be some controls for the search and a dialog close button; together with a list of placeholders for the album covers - these will eventually be loaded as the results of the search. + +To arrange the dialog controls, you will use the dock panel layout control, that is part of the _Avalonia UI_ built-in controls. This will keep the search controls at the top of the dialog, and the button at the bottom, whatever the height. The list will be the 'fill' area of the dock panel, so it will always take up all the remaining content zone. + +.A sketch of the dialog layout +image::_docs/13_search_album_dialog_sketch.png[A sketch showing how the dialog window will be laid out] + +NOTE: For full information on the dock panel control, see the reference https://docs.avaloniaui.net/docs/reference/controls/dockpanel[[here\]]. + +The dock panel itself will be located on an _Avalonia UI_ user control. This is so the code that shows the dialog can be separated from the code that operates the controls within the dialog. + +NOTE: This is a common pattern of UI Composition, to read about this concept, see https://docs.avaloniaui.net/docs/concepts/ui-composition[[here\]]. + +Follow this procedure to add the user control and constituent controls for the dialog: + +- Stop the app if it is still running. +- In the solution explorer, right-click the **/Views** folder and then click **Add**. +- Click **Avalonia User Control**. +- When prompted for the name, type 'MusicStoreView'. +- Press enter. +- Alter the XAML for the user control's content zone as follows: + +```xml + + + + + + +