C
C#2y ago
Denis

❔ [AvaloniaUI] Visibility of item in ListView based on condition

An Avalonia UI application is used to manage an evidence of items. It allows the user to add items. or remove them. The View displays a list of items in a ListView. The Item model represents an item in the evidence. The ViewModel for the View handles retrieving the list of Item instances. The View binds the observable list of Item instances to the ListView. The goal is to show the Remove button as part of a ListViewItem, but only when that given item in the ListView is selected. My idea is to only handle the collection of Item instances and the currently selected Item in the ViewModel, while the View would do the rest. I wanted to accomplish it via a Converter, that would receive the templated Item and the currently selected Item. However, after fighting with it for some time, I was unable to make it work. Is my thinking correct - should the View be responsible for handling the Remove button visibility? Should it be done in the ViewModel?
2 Replies
Denis
DenisOP2y ago
// Model
public partial class Item
: ObservableObject
{
[ObservableProperty]
private Guid _id = Guid.Empty;

[ObservableProperty]
private string _name = string.Empty;

[ObservableProperty]
private string _notes = string.Empty;
}
// Model
public partial class Item
: ObservableObject
{
[ObservableProperty]
private Guid _id = Guid.Empty;

[ObservableProperty]
private string _name = string.Empty;

[ObservableProperty]
private string _notes = string.Empty;
}
/// ViewModel
public partial class MainWindowViewModel
: ObservableObject
{
private Item? m_selectedItem;

public Item? SelectedItem
{
get => m_selectedItem;
set
{
if (Equals(value, m_selectedItem)) return;
m_selectedItem = value;
OnPropertyChanged();
DeleteItemCommand.NotifyCanExecuteChanged();
}
}

/// <summary>
/// Evidence of items
/// </summary>
public ObservableCollection<Item> Items { get; set; }

/// <summary>
/// Default constructor
/// </summary>
public MainWindowViewModel()
{
var items = new Faker<Item>()
.RuleFor(item => item.Id, Guid.NewGuid)
.RuleFor(item => item.Name, faker => faker.Name.FullName())
.RuleFor(item => item.Notes, string.Empty)
.Generate(10);

Items = new ObservableCollection<Item>(items);
}

[RelayCommand(CanExecute = nameof(CanDeleteItem))]
private void DeleteItem()
{
Students.Remove(SelectedItem);
}

private bool CanDeleteItem() => SelectedItem is not null;
}
/// ViewModel
public partial class MainWindowViewModel
: ObservableObject
{
private Item? m_selectedItem;

public Item? SelectedItem
{
get => m_selectedItem;
set
{
if (Equals(value, m_selectedItem)) return;
m_selectedItem = value;
OnPropertyChanged();
DeleteItemCommand.NotifyCanExecuteChanged();
}
}

/// <summary>
/// Evidence of items
/// </summary>
public ObservableCollection<Item> Items { get; set; }

/// <summary>
/// Default constructor
/// </summary>
public MainWindowViewModel()
{
var items = new Faker<Item>()
.RuleFor(item => item.Id, Guid.NewGuid)
.RuleFor(item => item.Name, faker => faker.Name.FullName())
.RuleFor(item => item.Notes, string.Empty)
.Generate(10);

Items = new ObservableCollection<Item>(items);
}

[RelayCommand(CanExecute = nameof(CanDeleteItem))]
private void DeleteItem()
{
Students.Remove(SelectedItem);
}

private bool CanDeleteItem() => SelectedItem is not null;
}
<Window
Icon="/Assets/avalonia-logo.ico"
Name="Root"
Title="TestApp"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d"
x:Class="TestApp.Views.MainWindow"
xmlns="https://github.com/avaloniaui"
xmlns:converters="clr-namespace:TestApp.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="clr-namespace:TestApp.Models"
xmlns:vm="using:TestApp.ViewModels"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

<Design.DataContext>
<vm:MainWindowViewModel />
</Design.DataContext>
<Grid ColumnDefinitions="Auto,*">
<ListBox
Grid.Column="0"
Items="{Binding Items}"
SelectedItem="{Binding SelectedItem}"
SelectionMode="Single">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type models:Item}">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0" Text="{Binding Name}" />
<Button
Command="{Binding DeleteItemCommand, ElementName=Root}"
Grid.Column="1"
Margin="5,0,0,0">
Delete
<Button.IsVisible>
<Binding Path="SelectedItem" RelativeSource="{RelativeSource AncestorType=Window}">
<Binding.Converter>
<converters:SelectedItemToBoolConverter />
</Binding.Converter>
</Binding>
</Button.IsVisible>
</Button>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>
<Window
Icon="/Assets/avalonia-logo.ico"
Name="Root"
Title="TestApp"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d"
x:Class="TestApp.Views.MainWindow"
xmlns="https://github.com/avaloniaui"
xmlns:converters="clr-namespace:TestApp.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="clr-namespace:TestApp.Models"
xmlns:vm="using:TestApp.ViewModels"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

<Design.DataContext>
<vm:MainWindowViewModel />
</Design.DataContext>
<Grid ColumnDefinitions="Auto,*">
<ListBox
Grid.Column="0"
Items="{Binding Items}"
SelectedItem="{Binding SelectedItem}"
SelectionMode="Single">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type models:Item}">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0" Text="{Binding Name}" />
<Button
Command="{Binding DeleteItemCommand, ElementName=Root}"
Grid.Column="1"
Margin="5,0,0,0">
Delete
<Button.IsVisible>
<Binding Path="SelectedItem" RelativeSource="{RelativeSource AncestorType=Window}">
<Binding.Converter>
<converters:SelectedItemToBoolConverter />
</Binding.Converter>
</Binding>
</Button.IsVisible>
</Button>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>
// Converter
public class SelectedItemToBoolConverter
: AvaloniaObject, IValueConverter
{
public static readonly StyledProperty<object?> SelectedItemProperty
= AvaloniaProperty.Register<SelectedItemToBoolConverter, object?>(nameof(SelectedItem));

public object? SelectedItem
{
get => GetValue(SelectedItemProperty);
set => SetValue(SelectedItemProperty, value);
}

/// <inheritdoc />
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
=> value is Item itm && SelectedItem is Item currentItm && itm.Id.Equals(currentItm.Id);

/// <inheritdoc />
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => throw new NotImplementedException();
}
// Converter
public class SelectedItemToBoolConverter
: AvaloniaObject, IValueConverter
{
public static readonly StyledProperty<object?> SelectedItemProperty
= AvaloniaProperty.Register<SelectedItemToBoolConverter, object?>(nameof(SelectedItem));

public object? SelectedItem
{
get => GetValue(SelectedItemProperty);
set => SetValue(SelectedItemProperty, value);
}

/// <inheritdoc />
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
=> value is Item itm && SelectedItem is Item currentItm && itm.Id.Equals(currentItm.Id);

/// <inheritdoc />
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => throw new NotImplementedException();
}
I confess, that the current converter implementation and use is incorrect; however, I wish to hear some of your advice, before I continue mashing my head against the wall to find the correct solution. The main question is whether my approach is even correct
Accord
Accord2y ago
Looks like nothing has happened here. I will mark this as stale and this post will be archived until there is new activity.
Want results from more Discord servers?
Add your server