Reputation: 33
I'm learning Avalonia and am trying to have an ItemsControl switch between two different DataTemplates based on a property in an ObservableCollection of custom objects.
I have an ObservableCollection<Parameter>
called "Parameters" with "Parameter" defined as follows:
public class Parameter
{
public Parameter(string value = "") {
if (value.ToLower() == "switch") {
IsBool = true;
}
}
private string _name;
public string Name {
get { return _name; }
set {
_name = value;
}
}
public bool IsBool { get; set; } = false;
}
This is my XAML code where the ItemsControl is defined:
<ItemsControl Grid.IsSharedSizeScope="True" ItemsSource="{Binding Parameters}">
<ItemsControl.ItemTemplate>
<Binding Path="IsBool">
<resources:ParameterTemplateSelector>
<DataTemplate x:Key="true">
<Grid Margin="20 10 ">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Label" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="*" MaxWidth="600" />
</Grid.ColumnDefinitions>
<Label Content="{ReflectionBinding Path=Name}" VerticalContentAlignment="Center"
HorizontalContentAlignment="Right" />
<ToggleSwitch Grid.Column="2"/>
</Grid>
</DataTemplate>
<DataTemplate x:Key="false">
<Grid Margin="20 10 ">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Label" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="*" MaxWidth="600" />
</Grid.ColumnDefinitions>
<Label Content="{ReflectionBinding Path=Name}" VerticalContentAlignment="Center"
HorizontalContentAlignment="Right" />
<AutoCompleteBox Name="AutoCompleteBox" Grid.Column="2"
HorizontalAlignment="Stretch"/>
</Grid>
</DataTemplate>
</resources:ParameterTemplateSelector>
</Binding>
</ItemsControl.ItemTemplate>
</ItemsControl>
And here's "ParameterTemplateConverter" (though the code gives the same error without it):
public class ParameterTemplateSelector : IDataTemplate
{
[Content]
public Dictionary<bool, IDataTemplate> AvailableTemplates { get; } = new Dictionary<bool, IDataTemplate>();
public Control Build(object? param)
{
var key = param?.Equals(true);
if (key == null) {
throw new ArgumentNullException(nameof(param));
}
return AvailableTemplates[key.Value].Build(param);
}
public bool Match(object? data)
{
var key = data?.Equals(true);
return data is bool
&& key != null
&& AvailableTemplates.ContainsKey(key.Value);
}
}
Ideally this would create a Grid with a ToggleSwitch for every "IsBool" with a value of true, and one with an AutoCompleteBox for the rest, but attempting to run the code gives me this error, presumably on the "IsBool" bound property:
Error AVLN:0004 Avalonia: Internal compiler error while transforming node XamlX.Ast.XamlAstObjectNode: System.ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection. (Parameter 'index')
I feel like this is because the collection is empty at runtime, but I don't get this error if I use any of the "Parameter" properties elsewhere, and since I can't populate the collection before runtime I'm not quite sure how to approach solving this.
Upvotes: 0
Views: 404
Reputation: 128077
There are mainly two things wrong in your code.
First, the <resources:ParameterTemplateSelector>
should not be declared inside a Binding. The XAML should look like this:
<ItemsControl ItemsSource="{Binding Parameters}" ...>
<ItemsControl.ItemTemplate>
<resources:ParameterTemplateSelector>
<DataTemplate x:Key="true">
...
</DataTemplate>
<DataTemplate x:Key="false">
...
</DataTemplate>
</resources:ParameterTemplateSelector>
</ItemsControl.ItemTemplate>
</ItemsControl>
Second, the code of the ParameterTemplateSelector
should look like shown below. I used #nullable disable
for brevity.
Please note that the object that is passed to the Build
method is the same as that passed to the Match
method. The method parameter should hence also be data
, because param
is confusing here. The name was just like that because the Build method is declared in a base interface with additional purposes.
#nullable disable
public class ParameterTemplateSelector : IDataTemplate
{
[Content]
public Dictionary<string, IDataTemplate> AvailableTemplates { get; }
= new Dictionary<string, IDataTemplate>();
public Control Build(object data)
{
// cast data to Parameter and access the property value
var parameter = (Parameter)data;
var key = parameter.IsBool.ToString().ToLower();
return AvailableTemplates[key].Build(parameter);
}
public bool Match(object data)
{
// test if data is an instance of Parameter
return data is Parameter;
}
}
As a general note, it seems pointless to introduce the IsBool
property in the first place. Better keep a string
property that could directly be used as a dictionary key.
Maybe declare the Parameter class like this:
public class Parameter
{
public string Name { get; set; }
public string Kind { get; set; }
}
and use the Kind
property as key:
public Control Build(object data)
{
var parameter = (Parameter)data;
return AvailableTemplates[parameter.Kind].Build(parameter);
}
with
<resources:ParameterTemplateSelector>
<DataTemplate x:Key="switch">
...
</DataTemplate>
<DataTemplate x:Key="acbox">
...
</DataTemplate>
</resources:ParameterTemplateSelector>
Upvotes: 1