Merge pull request #62 from daniel-lerch/master
Correct music store tutorial
This commit is contained in:
Коммит
c97c642c51
|
@ -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()
|
||||
|
|
Загрузка…
Ссылка в новой задаче