Merge pull request #62 from daniel-lerch/master

Correct music store tutorial
This commit is contained in:
Max Katz 2021-08-01 20:26:14 -04:00 коммит произвёл GitHub
Родитель a8f36f7912 c7bcced007
Коммит c97c642c51
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
4 изменённых файлов: 26 добавлений и 52 удалений

Просмотреть файл

@ -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
<DockPanel>
@ -127,34 +127,9 @@ Then bind its `Command` to `BuyMusicCommand`.
</DockPanel>
```
now return to your `MusicStoreViewModel.cs`
Add a BuyMusicCommand property like so:
```csharp
public ReactiveCommand<Unit, AlbumViewModel?> 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
<ListBox Items="{Binding SearchResults}" SelectedItem="{Binding SelectedAlbum}" Background="Transparent" Margin="0 20" />
@ -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.

Просмотреть файл

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

Просмотреть файл

@ -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<MusicStoreViewModel, AlbumViewModel?> interaction)

Просмотреть файл

@ -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<Unit, AlbumViewModel?> BuyMusicCommand { get; }
@ -20,30 +20,27 @@ Note we are using `ReactiveCommand` this is where we are using ReactiveUI to pro
Note that `ReactiveCommand<TParam, TResult>` 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<MusicStoreViewModel>` 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()