Add Content to Dialog
Adding Content to the Dialog
Inside the dialog we would like the user to search for albums, and then select an album to buy.
To do this we will need to query a web api to return a list of items. This will take some time so the user may need to wait.
The UI in order to achieve this can consist of the following elements.
- A
TextBox
for the user to type the album or artist name. - A
ProgressBar
to tell the user their search request is happening. - A
ListBox
to display a list of Albums and allow the user to select one. - A
Button
for the user to confirm their selection. Effectively buying the album.
Controls can be laid out inside Panels
. The most common ones are StackPanel
and DockPanel
.
A StackPanel
will stack multiple controls on top of each other or next to each other depending on the Orientation
that is set. The default being Vertical
.
A DockPanel
allows controls to be docked to any side the user wished via the DockPanel.Dock
attached property. The last control inside a DockPanel
if it doesn't have a Dock
specified will fill the remaining space.
Right click on your Views
folder and select Add
→ Avalonia User Control
to add a new View
to the project.
Name this MusicStoreView
and press the Enter
key when prompted.
Build the project so that the previewer will work.
Declare a <DockPanel>
.
<DockPanel>
</DockPanel>
Inside the DockPanel
add a <StackPanel>
. Set DockPanel.Dock="Top"
on the StackPanel
so that it will be positioned at the top.
<DockPanel>
<StackPanel DockPanel.Dock="Top">
</StackPanel>
</DockPanel>
Inside the StackPanel
add a TextBox
and a ProgressBar
.
<DockPanel>
<StackPanel DockPanel.Dock="Top">
<TextBox Text="{Binding SearchText}" Watermark="Search for Albums...." />
<ProgressBar IsIndeterminate="True" IsVisible="{Binding IsBusy}" />
</StackPanel>
</DockPanel>
Set the properties as shown. Notice we have bound the Text
property of the TextBox
to a property called SearchText
. The View
will expect to find this property on our MusicStoreViewModel
and will keep that property in sync with whatever the user types. We also added a binding
for the IsVisible
to the IsBusy
property.
To add these properties
to our ViewModel
open MusicStoreViewModel.cs
. Make the class inherit ViewModelBase
, so that it is capable of notifying changes.
Then add the following code:
using ReactiveUI;
namespace Avalonia.MusicStore.ViewModels
{
public class MusicStoreViewModel : ViewModelBase
{
private string? _searchText;
public string? SearchText
{
get => _searchText;
set => this.RaiseAndSetIfChanged(ref _searchText, value);
}
}
}
Here we can see that we have a normal getter which returns the field, and we have a Setter that calls a method.
This calls RaiseAndSetIfChanged
this method checks to see if value
is different from the current _seachText
field and if so, it changes it to the new value, and then raises an event to notify the View
that it has changed.
Any View that has a {Binding SearchText}
expression will automatically update, and stay in sync with this property.
Also add a boolean property named IsBusy
with the same convention.
using ReactiveUI;
namespace Avalonia.MusicStore.ViewModels
{
public class MusicStoreViewModel : ViewModelBase
{
private bool _isBusy;
private string? _searchText;
public string? SearchText
{
get => _searchText;
set => this.RaiseAndSetIfChanged(ref _searchText, value);
}
public bool IsBusy
{
get => _isBusy;
set => this.RaiseAndSetIfChanged(ref _isBusy, value);
}
}
}
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
which we will create in the next chapter.
<DockPanel>
<StackPanel DockPanel.Dock="Top">
<TextBox Text="{Binding SearchText}" Watermark="Search for Albums...." />
<ProgressBar IsIndeterminate="True" IsVisible="{Binding IsBusy}" />
</StackPanel>
<Button Command="{Binding BuyMusicCommand}" Content="Buy Album" DockPanel.Dock="Bottom" HorizontalAlignment="Center" />
</DockPanel>
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
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.
<ListBox Items="{Binding SearchResults}" SelectedItem="{Binding SelectedAlbum}" Background="Transparent" Margin="0 20" />
As you might imagine, the Items
property needs some kind of List to bind to and the SelectedItem
property needs some object to bind to also. However this time they will not be simple types like bool
, int
or string
. They will be ViewModels
, this is why we created the AlbumViewModel
earlier.
Return to the MusicStoreViewModel.cs
file and add the following code.
private AlbumViewModel? _selectedAlbum;
public ObservableCollection<AlbumViewModel> SearchResults { get; } = new();
public AlbumViewModel? SelectedAlbum
{
get => _selectedAlbum;
set => this.RaiseAndSetIfChanged(ref _selectedAlbum, value);
}
As you can see the SelectedAlbum
property is implemented with the by now familiar pattern.
The SearchResults
property does not require this pattern and is a special type, called ObservableCollection<T>
. This comes from the using System.Collections.ObjectModel;
namespace.
An observable collection is simply a List
or Collection
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 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
.
The ListBox
will see that the SearchResults
has an item inside it, it will check the type of the item, which will be AlbumViewModel
. The ListBox
will then see if it has a DataTemplate
for that type, which we don't. However it will find at the root of the application in App.axaml
.
In order to do that we will create a special class named ViewLocator
. Right click on project and Add
→ Class/Interface
:
using System;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.MusicStore.ViewModels;
namespace Avalonia.MusicStore
{
public class ViewLocator : IDataTemplate
{
public bool SupportsRecycling => false;
public IControl Build(object data)
{
var name = data.GetType().FullName!.Replace("ViewModel", "View");
var type = Type.GetType(name);
if (type != null)
{
return (Control) Activator.CreateInstance(type)!;
}
else
{
return new TextBlock {Text = "Not Found: " + name};
}
}
public bool Match(object data)
{
return data is ViewModelBase;
}
}
}
And then add that to App.axaml
:
<Application.DataTemplates>
<local:ViewLocator />
</Application.DataTemplates>
This ViewLocator is a special DataTemplate
that will take the typename, in this case Avalonia.MusicStore.ViewModels.AlbumViewModel
and check to see if a View
exists at Avalonia.MusicStore.Views.AlbumView
.
If the ListBox finds one, it will display whatever xaml
that View
describes inside the ListBox
, otherwise it will just show the typename inside the ListBox
as a string.
Lets test this by adding a contructor on the MusicStoreViewModel.cs
.
public MusicStoreViewModel()
{
SearchResults.Add(new AlbumViewModel());
SearchResults.Add(new AlbumViewModel());
SearchResults.Add(new AlbumViewModel());
}
Before we can run this we need to add our MusicStoreView
to our MusicStoreWindow
which is currently empty.
At the top of MusicStoreWindow.axaml
you will find some lines that begin xmlns:x
etc.. add a line:
xmlns:local="using:Avalonia.MusicStore.Views"
Then inside the <Panel>
add.
<local:MusicStoreView />