From c7bcced0076267f08cbc49934033986fddaf8f6f Mon Sep 17 00:00:00 2001 From: Daniel Lerch Date: Sun, 1 Aug 2021 23:18:06 +0200 Subject: [PATCH] Correct music store tutorial --- .../music-store-app/add-content-to-dialog.md | 35 +++---------------- .../music-store-app/displaying-images.md | 16 +++++---- tutorials/music-store-app/opening-a-dialog.md | 6 ++-- .../music-store-app/return-from-dialog.md | 21 +++++------ 4 files changed, 26 insertions(+), 52 deletions(-) diff --git a/tutorials/music-store-app/add-content-to-dialog.md b/tutorials/music-store-app/add-content-to-dialog.md index f947fae..c5a36bf 100644 --- a/tutorials/music-store-app/add-content-to-dialog.md +++ b/tutorials/music-store-app/add-content-to-dialog.md @@ -111,11 +111,11 @@ namespace Avalonia.MusicStore.ViewModels } ``` -Return to the \`MusicStoreView.axaml. So that we can add the remaining controls. +Return to the `MusicStoreView.axaml`. So that we can add the remaining controls. Back inside our DockPanel add a `Button` and set it to Dock at the bottom. Set its `Content` to "Buy Album" its `HorizontalAlignment` to `Center`. -Then bind its `Command` to `BuyMusicCommand`. +Then bind its `Command` to `BuyMusicCommand` which we will create in the next chapter. ```markup @@ -127,34 +127,9 @@ Then bind its `Command` to `BuyMusicCommand`. ``` -now return to your `MusicStoreViewModel.cs` - -Add a BuyMusicCommand property like so: - -```csharp -public ReactiveCommand BuyMusicCommand { get; } -``` - -In the contructor add the following: - -```csharp -BuyMusicCommand = ReactiveCommand.CreateFromTask(async () => -{ - if (SelectedAlbum is { }) - { - return SelectedAlbum; - } - return null; -}); -``` - -This will cause the `BuyMusicCommand` to return the `SelectedAlbum`'s View Model to the main window if there is a selection. Otherwise, it will return `null`. - -Return to the `MusicStoreView.axaml` - Add a `ListBox` to the `DockPanel`. Since this is the last item in the Panel it will fill the remaining space, and since the `TextBox` and `ProgressBar` are docked to the top inside a `StackPanel` and the `Button` is docked to the bottom. This ListBox will appear in between them and fill the space. -Bind the `Items` and `SelectedItem` properties as shown, set the `Background` to `Transparent`. Add a `Margin` or `0 20`. This mean left and right sides have 0 and top and bottom have 20. This creates some space between the other controls. +Bind the `Items` and `SelectedItem` properties as shown, set the `Background` to `Transparent`. Add a `Margin` of `0 20`. This means left and right sides have 0 and top and bottom have 20. This creates some space between the other controls. ```markup @@ -182,7 +157,7 @@ The `SearchResults` property does not require this pattern and is a special type An observable collection is simply a `List` or `Colleciton` that when items are added or removed from it, it fires `events` so other code can be notified of changes to the list. -Notice this property is instantiated with `= new ();`. Forget this and it will be `null` and wont work. +Notice this property is instantiated with `= new ();`. Forget this and it will be `null` and won't work. Since we are using `ObservableCollection` when we `bind` the `ListBox`s `Items` property to it, then the `ListBox` control will start listening to events and keep the `Items` inside the `ListBox` in sync with the `ObservableCollection` on the `ViewModel`. @@ -333,5 +308,5 @@ Now when we run the application we get: As our list gets more items the will wrap around onto the next line, and the user will be able to scroll. -This is a very powerful and flexible feature in Avalonia. Any layout can be acheived, by implementing your own `Panel` class. However that is outside the scope of this tutorial. +This is a very powerful and flexible feature in Avalonia. Any layout can be achieved, by implementing your own `Panel` class. However that is outside the scope of this tutorial. diff --git a/tutorials/music-store-app/displaying-images.md b/tutorials/music-store-app/displaying-images.md index 782c63a..55cbfba 100644 --- a/tutorials/music-store-app/displaying-images.md +++ b/tutorials/music-store-app/displaying-images.md @@ -93,7 +93,7 @@ private async void LoadCovers(CancellationToken cancellationToken) } ``` -Calling this asynchronous method will itereate through each item in the `SearchResults` and call our `AlbumViewModel`s `LoadCover` method. +Calling this asynchronous method will itereate through each item in a copy of the `SearchResults` and call our `AlbumViewModel`s `LoadCover` method. Creating a copy with `.ToList()` is necessary because this method is async and `SearchResults` might be updated by another thread. Notice a `CancellationToken` is used to check if we want to stop loading album covers. @@ -102,16 +102,17 @@ Now add the following code to the beggining of `DoSearch` method of `MusicStoreV ```csharp _cancellationTokenSource?.Cancel(); _cancellationTokenSource = new CancellationTokenSource(); +var cancellationToken = _cancellationTokenSource.Token; ``` -This will mean that if there is an existing request still loading Album art, we can cancel them. +If there is an existing request still loading Album art, we can cancel it. Because `_cancellationTokenSource` might be replaced asynchronously we have to store the cancellation token in a local variable. Now add the following code to the end of `DoSearch` method of `MusicStoreViewModel` before the `IsBusy = false;` line. ```csharp -if (!_cancellationTokenSource.IsCancellationRequested) +if (!cancellationToken.IsCancellationRequested) { - LoadCovers(_cancellationTokenSource.Token); + LoadCovers(cancellationToken); } ``` @@ -123,8 +124,9 @@ private async void DoSearch(string s) IsBusy = true; SearchResults.Clear(); - _cancellationTokenSource?.Cancel(); + _cancellationTokenSource?.Cancel(); _cancellationTokenSource = new CancellationTokenSource(); + var cancellationToken = _cancellationTokenSource.Token; var albums = await Album.SearchAsync(s); @@ -135,9 +137,9 @@ private async void DoSearch(string s) SearchResults.Add(vm); } - if (!_cancellationTokenSource.IsCancellationRequested) + if (!cancellationToken.IsCancellationRequested) { - LoadCovers(_cancellationTokenSource.Token); + LoadCovers(cancellationToken); } IsBusy = false; diff --git a/tutorials/music-store-app/opening-a-dialog.md b/tutorials/music-store-app/opening-a-dialog.md index a5a4a16..16e8b0f 100644 --- a/tutorials/music-store-app/opening-a-dialog.md +++ b/tutorials/music-store-app/opening-a-dialog.md @@ -143,12 +143,12 @@ Once the dialog has closed, it will return the result, which will be of type `Al * Add the following `WhenActivated` call to the Windows constructor. ```csharp -this.WhenActivated(d => d(ViewModel.ShowDialog.RegisterHandler(DoShowDialogAsync))); +this.WhenActivated(d => d(ViewModel!.ShowDialog.RegisterHandler(DoShowDialogAsync))); ``` `d` is an `Action` that takes a `disposable`, this means that ReactiveUI will clean up any subscriptions when this View is not on the screen for us. -Our entire \`MainWindow.xaml.cs should now look like: +Our entire `MainWindow.xaml.cs` should now look like: ```csharp using System.Threading.Tasks; @@ -169,7 +169,7 @@ namespace Avalonia.MusicStore.Views #if DEBUG this.AttachDevTools(); #endif - this.WhenActivated(d => d(ViewModel.ShowDialog.RegisterHandler(DoShowDialogAsync))); + this.WhenActivated(d => d(ViewModel!.ShowDialog.RegisterHandler(DoShowDialogAsync))); } private async Task DoShowDialogAsync(InteractionContext interaction) diff --git a/tutorials/music-store-app/return-from-dialog.md b/tutorials/music-store-app/return-from-dialog.md index 4a4ce48..f3aec47 100644 --- a/tutorials/music-store-app/return-from-dialog.md +++ b/tutorials/music-store-app/return-from-dialog.md @@ -4,13 +4,13 @@ Now that the user can select one of our Albums, we need to be able to close the Dialog and return the result to the `ViewModel` that called the dialog. -Notice that our `MusicStoreViewModel` has a `SelectedAlbum` property that we added previously and that the `ListBox` on the `MusicStoreView` has its `SelectedItem` property bound to this `SelectedAlbum` property of the `viewmodel`. +Notice that our `MusicStoreViewModel` has a `SelectedAlbum` property that we added previously and that the `ListBox` on the `MusicStoreView` has its `SelectedItem` property bound to this `SelectedAlbum` property of the view model. This means that when the user clicks to select an album, the listbox shows that it is selected by highlighting the item. At the same time the `SelectedAlbum` property will be kept in sync and set to the `AlbumViewModel` instance that represents the `SelectedItem` of the `ListBox`. -The `Button` of the `MusicStoreView` has its `Command` property bound to `BuyMusicCommand`. This doesnt exist yet so lets add this to `MusicStoreViewModel` with the following code. +The `Button` of the `MusicStoreView` has its `Command` property bound to `BuyMusicCommand`. This doesn't exist yet so lets add this to `MusicStoreViewModel` with the following code. ```csharp public ReactiveCommand BuyMusicCommand { get; } @@ -20,30 +20,27 @@ Note we are using `ReactiveCommand` this is where we are using ReactiveUI to pro Note that `ReactiveCommand` has some type arguments. Commands can take a parameter, however we do not need a paramter in this case, so we use `Unit` which is kind of a dummy type, it contains no data. Reactive Commands can also return a result. This will be useful for returning the Album the user wants to buy. -Now add a constructor to `MusicStoreViewModel` where we can instantiate the command, and implement the code needed to return a result from the dialog. +Now add the following lines to the constructor of `MusicStoreViewModel` in order to instantiate the command and implement the code needed to return a result from the dialog: ```csharp -public MusicStoreViewModel() +BuyMusicCommand = ReactiveCommand.Create(() => { - BuyMusicCommand = ReactiveCommand.Create(() => - { - return SelectedAlbum; - }); -} + return SelectedAlbum; +}); ``` Simply when the button is clicked, this code will execute, and return the value assigned to `SelectedAlbum`. So far so good, but how does the actual dialog get closed? -To close the dialog, we need to open MusicStoreWindow.axaml.cs and make a few changes. +To close the dialog, we need to open `MusicStoreWindow.axaml.cs` and make a few changes. Firstly make it inherit `ReactiveWindow` so that ReactiveUI can help us out. Then add the following line to the end of the constructor. ```csharp -this.WhenActivated(d => d(ViewModel.BuyMusicCommand.Subscribe(Close))); +this.WhenActivated(d => d(ViewModel!.BuyMusicCommand.Subscribe(Close))); ``` This line says when the Window is activated \(becomes visible on the screen\), the lambda expression will be called. @@ -76,7 +73,7 @@ namespace Avalonia.MusicStore.Views this.AttachDevTools(); #endif - this.WhenActivated(d => d(ViewModel.BuyMusicCommand.Subscribe(Close))); + this.WhenActivated(d => d(ViewModel!.BuyMusicCommand.Subscribe(Close))); } private void InitializeComponent()