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`.
|
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
|
```markup
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
|
@ -127,34 +127,9 @@ Then bind its `Command` to `BuyMusicCommand`.
|
||||||
</DockPanel>
|
</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.
|
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
|
```markup
|
||||||
<ListBox Items="{Binding SearchResults}" SelectedItem="{Binding SelectedAlbum}" Background="Transparent" Margin="0 20" />
|
<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.
|
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`.
|
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.
|
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.
|
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
|
```csharp
|
||||||
_cancellationTokenSource?.Cancel();
|
_cancellationTokenSource?.Cancel();
|
||||||
_cancellationTokenSource = new CancellationTokenSource();
|
_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.
|
Now add the following code to the end of `DoSearch` method of `MusicStoreViewModel` before the `IsBusy = false;` line.
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
if (!_cancellationTokenSource.IsCancellationRequested)
|
if (!cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
LoadCovers(_cancellationTokenSource.Token);
|
LoadCovers(cancellationToken);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -123,8 +124,9 @@ private async void DoSearch(string s)
|
||||||
IsBusy = true;
|
IsBusy = true;
|
||||||
SearchResults.Clear();
|
SearchResults.Clear();
|
||||||
|
|
||||||
_cancellationTokenSource?.Cancel();
|
_cancellationTokenSource?.Cancel();
|
||||||
_cancellationTokenSource = new CancellationTokenSource();
|
_cancellationTokenSource = new CancellationTokenSource();
|
||||||
|
var cancellationToken = _cancellationTokenSource.Token;
|
||||||
|
|
||||||
var albums = await Album.SearchAsync(s);
|
var albums = await Album.SearchAsync(s);
|
||||||
|
|
||||||
|
@ -135,9 +137,9 @@ private async void DoSearch(string s)
|
||||||
SearchResults.Add(vm);
|
SearchResults.Add(vm);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_cancellationTokenSource.IsCancellationRequested)
|
if (!cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
LoadCovers(_cancellationTokenSource.Token);
|
LoadCovers(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
IsBusy = false;
|
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.
|
* Add the following `WhenActivated` call to the Windows constructor.
|
||||||
|
|
||||||
```csharp
|
```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.
|
`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
|
```csharp
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
@ -169,7 +169,7 @@ namespace Avalonia.MusicStore.Views
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
this.AttachDevTools();
|
this.AttachDevTools();
|
||||||
#endif
|
#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)
|
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.
|
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.
|
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`.
|
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
|
```csharp
|
||||||
public ReactiveCommand<Unit, AlbumViewModel?> BuyMusicCommand { get; }
|
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.
|
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
|
```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`.
|
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?
|
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.
|
Firstly make it inherit `ReactiveWindow<MusicStoreViewModel>` so that ReactiveUI can help us out.
|
||||||
|
|
||||||
Then add the following line to the end of the constructor.
|
Then add the following line to the end of the constructor.
|
||||||
|
|
||||||
```csharp
|
```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.
|
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();
|
this.AttachDevTools();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
this.WhenActivated(d => d(ViewModel.BuyMusicCommand.Subscribe(Close)));
|
this.WhenActivated(d => d(ViewModel!.BuyMusicCommand.Subscribe(Close)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeComponent()
|
private void InitializeComponent()
|
||||||
|
|
Загрузка…
Ссылка в новой задаче