In an AvaloniaUI window, I want to have a TabControl whose tabs are added and removed from an ObservableCollection<T>. The tab's "title" (the text appearing on the tab strip) should be set inside each item of the collection, which could belong to a different type.
For that I defined a type:
public abstract class TabViewModelBase : ViewModelBase
{
public abstract string TabHeader { get; }
}
and my collection is defined like this:
public ObservableCollection<TabViewModelBase> OpenTabs { get; } = new();
In the axaml file, this is the definition of the TabControl:
<TabControl Items="{Binding OpenTabs}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding TabHeader}"/>
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
So far, this works like a charm.
The problem begins when I also want to set up a container for the view inside each tab, which should not be a part of the contained view itself. I've tried by editing the xaml above and setting a ContentTemplate like this:
<TabControl Items="{Binding OpenTabs}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding TabHeader}"/>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<Border Child="{Binding}"/>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
However this results in the following error:
[Binding] Error in binding to 'Avalonia.Controls.Border'.'Child': 'Could not convert 'Project.ViewModels.TestingViewModel' to 'IControl'.'
This seems to be because ViewLocator, which automatically matches a view model to a view based on its name, is not being called. I assume this is because I've defined a DataTemplate inside TabControl.ContentTemplate.
Is it possible to instruct Avalonia to use ViewLocator inside TabControl.ContentTemplate, so that a view is selected based on its name?
<Border Child="{Binding}"/>
Border expects an actual control as a child, not a view model. You need to use ContentControl instead. It can also have it's own data template or view locator.
I found a way to work around the issue, by defining an IValueConverter that uses ViewLocator internally:
public class ViewModelValueConverter : IValueConverter
{
public object? Convert(
object value, Type targetType, object parameter, CultureInfo culture
)
{
if (value == null)
return null;
if (
value is ViewModelBase viewModel
&& targetType.IsAssignableFrom(typeof(IControl))
)
{
ViewLocator viewLocator = new();
return viewLocator.Build(value);
}
throw new NotSupportedException();
}
public object ConvertBack(
object value, Type targetType, object parameter, CultureInfo culture
)
{
throw new NotSupportedException();
}
}
and using it in XAML:
<Window.Resources>
<local:ViewModelValueConverter x:Key="variableView"/>
</Window.Resources>
<TabControl Items="{Binding OpenTabs}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding TabHeader}"/>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<Border Child="{Binding, Converter={StaticResource variableView}}"/>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
but it feels like there might be a simpler solution.
Related
I've had a go at using a template selector with a TreeView (see this project: https://github.com/imekon/AvaloniaTreeViewTemplate)
It creates a tree but the node no longer has an arrow to allow me to see the child nodes.
This is the XAML I used:
<TreeView Items="{Binding Things}">
<TreeView.Items>
<scg:List x:TypeArguments="vm:ThingViewModel">
<vm:ThingViewModel Template="Folder"/>
<vm:ThingViewModel Template="Thing"/>
</scg:List>
</TreeView.Items>
<TreeView.DataTemplates>
<helpers:ThingTemplateSelector>
<TreeDataTemplate x:Key="Folder" ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Name}"/>
</TreeDataTemplate>
<TreeDataTemplate x:Key="Thing">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Address}"/>
</StackPanel>
</TreeDataTemplate>
</helpers:ThingTemplateSelector>
</TreeView.DataTemplates>
</TreeView>
and this is the selector:
public class ThingTemplateSelector : IDataTemplate
{
public bool SupportsRecycling => false;
[Content]
public Dictionary<string, IDataTemplate> Templates { get; } = new Dictionary<string, IDataTemplate>();
public IControl Build(object data)
{
return Templates[((ThingViewModel)data).Template].Build(data);
}
public bool Match(object data)
{
return data is ThingViewModel;
}
}
I can get a tree view working if I use a simple TreeDataTemplate but then I can't get it to select the correct template for the type of entry (a 'thing' or a 'folder'), i.e. all the nodes are one type.
Is this possible with Avalonia right now?
The picture above shows the 'Thing' node but no arrow next to it. Click on it simply selects the item but you can't expand it to see the child node.
This is my XAML that is trying to have my ListView pass a parameter to the ViewModel command.
xmlns:mvvm="http://www.galasoft.ch/mvvmlight"
<ListBox x:Name="MyListView" ItemsSource="{Binding Objects}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<mvvm:EventToCommand Command="{Binding MyCommand}"
PassEventArgsToCommand="True"
EventArgsConverter="{StaticResource ParamConverter }"
EventArgsConverterParameter ="{Binding Name}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
And this is my converter:
public class ParamConverter : IEventArgsConverter
{
public object Convert(object value, object parameter)
{
var args = (SelectionChangedEventArgs)value;
var name = parameter as string;
return (string)name.ToString();
}
}
But I got the following error:
The resource "ParamConverter" could not be resolved
Your converter is not declared in XAML, you should add something like
<ListBox.Resources>
<yournamespace:ParamConverter x:Key="ParamConverter"/>
</ListBox.Resources>
inside your listbox tags.
EDIT: I'm not an expert at all, so bear with my "imprecise" terms if any. I think you're missing something: there is no magic that allows your XAML to be aware of your C#. You need to tell XAML that somewhere in your code (in yournamespace) there will be a ParamConverter object, that can be referenced inside xaml with ParamConverter key.
You can declare your resource locally inside ListBox tags as suggested, or at outer scope if needed.
Once resource is declared inside XAML, you can access it via StaticResource.
I want to display some text in a Xamarin forms ListView grid, based on the comparison of 2 date properties. I have a converter to compare the dates and return a string. What I am trying to do (without success) is pass the entire object to the converter.
XAML:
<ResourceDictionary>
<converters1:CancelConverter x:Key="CancelConverter" />
</ResourceDictionary>
...
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Grid>
<Label Grid.Column="0" Text="{Binding ., Converter={StaticResource CancelConverter}}" />
</Grid>
</ViewCell>
</DataTemplate>
Converter:
public class CancelConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
Schedule schedule = ((Schedule)value);
DateTime date1 = schedule.ProposedCollectionDate;
DateTime date2 = schedule.OrderDate;
if (date1.CompareTo(date2) < 0)
{
return "Cancel this order";
}
}
}
The problem is that in the converter, value is null. How can I pass the object to the Converter, instead of just a single property of the object?
What you have shown looks good to me, but there may be something bad in what you've hidden. I can't tell, but are you properly setting the binding context of the ListView i.e. are you setting the ItemsSource property? From the information you've given, you should be setting it to something like an ObservableCollection<Schedule> or IEnumerable<Schedule> and then each item in the ListView would have a Schedule as its binding context and it would make its way into the value converter happily.
I'm working on a view (called 'Familify') which shows users a list of assets, and allows them to delete an asset from the list. The assets are stored in an ObservableCollection in the ViewModel, so the command to delete simply takes the asset object and removes it from collection. I'm having issues getting the 'delete' functionality working. Here is the XAML and codebehind:
Familify.xaml
<ListView
ItemsSource="{Binding Assets}">
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80px" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="150px" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="60px" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Text="{Binding number}" FontFamily="Consolas"/>
<TextBlock
Grid.Column="1"
Text="{Binding type}"/>
<TextBlock
Grid.Column="2"
Text="add binding here"/>
<TextBlock
Grid.Column="3"
Text="add binding here"/>
<Button
Command="{x:Bind ViewModel.RemoveAssetCommand}"
CommandParameter="{Binding}"
Content=""
FontFamily="Segoe MDL2 Assets"
Grid.Column="4">
</Button>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Familify.xaml.cs
namespace asset_manager.Views
{
public sealed partial class Familify : UserControl
{
FamilifyViewModel ViewModel { get; set; }
public Familify()
{
this.InitializeComponent();
DataContextChanged += (s, e) =>
{
ViewModel = DataContext as FamilifyViewModel;
};
}
}
}
The idea is that clicking the button removes the asset from the list. (Just to note, the normal binding showing number, type, etc. is working correctly.) My thinking so far:
Try to use binding to access the RemoveAssetCommand stored in the View Model for the page. However, I couldn't get ancestral binding to work (i.e. trying to find the data context of an element higher up in the XAML hierarchy didn't work because findAncestor isn't a thing in UWP.)
x:Bind looked like a good solution, because it uses an explicit path to the property. So, if I declared ViewModel in my code behind, I could use x:Bind ViewModel.property. All well and good. I did just that, and intellisense allowed me to access the ViewModel.RemoveAssetCommand when typing it out.
However, this did not work, because I get the error no DataType defined for DataTemplate. This makes sense, so I tried two things.
x:DataType="Models:Asset" (put in the DataTemplate tag above) is the model being shown in the data template, so I tried that first. Of course, the command is not declared in the model, it's declared in the View Model, so that didn't work.
I instead tried x:DataType="ViewModels:FamilifyViewModel", thinking I could just use x:Bind with that. However, I then got an error that it couldn't cast an object of type Asset to FamilifyViewModel. This makes sense, because the object getting passed to this data template is of the type Asset.
This is a pain, because the whole reason I thought x:Bind would work is that I could just access the property directly from the ViewModel in the codebehind.
Explicitly stated, 1) is it possible to use x:Bind within a data template to access a base level property (in this case, a Prism command) on the ViewModel? and 2) is there a better way to go about implementing this functionality?
Is it possible to use x:Bind within a data template to access a base level property (in this case, a Prism command) on the ViewModel?
Yes, if you want to access a base level, you can reassign DataContext of button like following:
<Button DataContext="{Binding ElementName=Familily, Path=DataContext}"/>
The Family is the name of UserControl.
is there a better way to go about implementing this functionality?
When you put commad in the ViewModel and bind the button as above. The the bind item of button will become Family DataContext. So you could not invoke delete action directly in the ViewModel.
The best practice to implement this functionality is that put the RemoveAssetCommand in the Asset class. And use the ItemsSource of ListView as Button CommandParameter.
<Button
Command="{Binding RemoveAssetCommand}"
CommandParameter="{Binding ElementName=MyListView, Path=ItemsSource}"
Content=""
FontFamily="Segoe MDL2 Assets"
Grid.Column="4">
</Button>
Asset.cs
public class Asset
{
public string number { get; set; }
public string type { get; set; }
public ICommand RemoveAssetCommand
{
get
{
return new CommandHandler<ObservableCollection<Asset>>((item) => this.RemoveAction(item));
}
}
private void RemoveAction(ObservableCollection<Asset> items)
{
items.Remove(this);
}
}
ViewModel.cs
public class FamilifyViewModel
{
public ObservableCollection<Asset> Assets = new ObservableCollection<Asset>();
public FamilifyViewModel()
{
Assets.Add(new Asset { number = "100001", type = "hello" });
Assets.Add(new Asset { number = "100001", type = "hello" });
Assets.Add(new Asset { number = "100001", type = "hello" });
Assets.Add(new Asset { number = "100001", type = "hello" });
}
}
I have a form with some validations set in entity metadata class. and then binding entity instance to UI by VM. Something as below:
Xaml like:
<Grid x:Name="LayoutRoot">
<StackPanel VerticalAlignment="Top">
<input:ValidationSummary />
</StackPanel>
<TextBox Text="{Binding Name, Mode=TwoWay}" />
<ComboBox x:Name="xTest" ItemsSource="{Binding MyList}"
SelectedItem="{Binding MyItem,Mode=TwoWay,
DisplayMemberPath="MyName"
ValidatesOnDataErrors=True,
ValidatesOnNotifyDataErrors=True,
ValidatesOnExceptions=True,
NotifyOnValidationError=True,UpdateSourceTrigger=Explicit}" />
</Grid>
Code-behind like:
public MyForm()
{
InitializeComponent();
this.xTest.BindingValidationError +=new EventHandler<ValidationErrorEventArgs>((s,e)=>{
BindingExpression be = this.xTest.GetBindingExpression(ComboBox.SelectedItemProperty);
be.UpdateSource();
if (e.Action == ValidationErrorEventAction.Added)
((ComboBox)s).Foreground = new SolidColorBrush(Colors.Red);
});
}
Metadata like:
[Required]
public string Name { get; set; }
[RequiredAttribute]
public int MyItemID { get; set; }
But when running the app, I got nothing display in valudationSummary.
For CombBox, even there is error, looks like BindingValidationError event is never fired.
How to resolve it?
Why are you using an Explicit UpdateSourceTrigger?
Silverlight validation happens inside the binding framework, when the binding is updating the source object. The way you have this, there won't be a binding validation error because you never tell the binding to update the source object. Well, actually you do, but it happens inside the validation error event handler. You've written chicken-and-egg code.
Remove your UpdateSourceTrigger on your binding or set it to Default.
Remove the explicit call to BindingExpression.UpdateSource.
Remove setting the ComboBox foreground to red - you are using NotifyOnValidationError=True, which eliminates any need to manually color the control.
Remove the DisplayMemberPath from the binding
So your XAML:
<Grid x:Name="LayoutRoot">
<StackPanel VerticalAlignment="Top">
<input:ValidationSummary />
<ComboBox x:Name="xTest" ItemsSource="{Binding MyList}"
SelectedItem="{Binding MyItem,
Mode=TwoWay,
ValidatesOnDataErrors=True,
ValidatesOnNotifyDataErrors=True,
ValidatesOnExceptions=True,
NotifyOnValidationError=True}" />
</StackPanel>
</Grid>
And your code:
public MyForm()
{
InitializeComponent();
// you don't need anything here to have the validations work
}