I have a model TestItem : IBaseItem
, custom control TestCard
of which x:DataType = "TestItem"
.
I also have a MainViewModel
, which has an ObservableCollection<IBaseItem> BrowsingItems
In the MainPage, there is a resource dictionary with DataTemplates, one of which contains the custom TestCard and its TouchBehaviour from the Community Tookit. The data from the BrowsingItems are bound to a CollectionView with the DataTemplateSelector which compares based on the if the IBaseItem is TestItem or not.
<DataTemplate x:Key="TestTemplate"
>
<controls:TestCard >
<controls:TestCard.Behaviors>
<toolkit:TouchBehavior x:DataType="vm:TestItem"
LongPressCommand="{Binding Source={x:Reference mainPage}, Path=BindingContext.ShowTestContextPopupCommand}"
LongPressCommandParameter="{Binding Mode=TwoWay}"
LongPressDuration="750" />
</controls:TestCard.Behaviors>
</controls:TestCard>
</DataTemplate>
<DataTemplate x:Key="FolderTemplate">
<controls:FolderCard />
</DataTemplate>
<controls:BrowsingItemDataTemplateSelector x:Key="BrowsingTemplateSelector"
TemplateTest="{StaticResource TestTemplate}"
TemplateFolder="{StaticResource FolderTemplate}" />
<CollectionView Grid.Row="1"
x:Name="browsingView"
ItemsSource="{Binding BrowsingItems}"
ItemTemplate="{StaticResource BrowsingTemplateSelector}">
</CollectionView>
.
.
.
.
<Grid Grid.Column="0"
RowDefinitions="*,*"
RowSpacing="5">
<controls:GoldMenuAddButton Image="folder_gold.svg"
Clicked="{Binding AddFolderCommand}"
Grid.Row="0" />
<controls:GoldMenuAddButton Image="test_gold.svg"
Clicked="{Binding AddTestCommand}"
Grid.Row="1" />
</Grid>
The first Command is executed when holding mousepress on the TestCard. It displays a Popup with the Name
of the item, and a set of commands to choose from (enum), which I then plan on executing back in the MainViewModel.
The second Command is for adding new TestItems based on a name chosen in another Popup. The Command is used in a binding with another button on the page.
public partial class MainViewModel : ObservableObject
{
public MainViewModel()
{
BrowsingItems = [];
}
public ObservableCollection<IBaseItem> BrowsingItems { get; private set; }
void Sort()
{
var temp = BrowsingItems.OrderByDescending(x => x is FolderItem).ToList();
BrowsingItems.Clear();
foreach (var e in temp) BrowsingItems.Add(e);
}
[RelayCommand]
async void AddTest()
{
var popup = new CreateBrowsingItemPopup();
var name = await Application.Current.MainPage.ShowPopupAsync(popup);
if (name != null)
{
BrowsingItems.Add(new TestItem() { Name = (string)name, QuestionsAmount = 0, QuestionsAnswered = 0, Random = false });
Sort();
}
}
int folderCount = 0;
int testCount = 0;
[RelayCommand]
async void AddFolder()
{
var popup = new CreateBrowsingItemPopup();
var name = await Application.Current.MainPage.ShowPopupAsync(popup);
if (name != null)
{
BrowsingItems.Add(new FolderItem() { Name = (string)name });
Sort();
}
}
[RelayCommand]
async void ShowTestContextPopup(TestItem testItem)
{
var popup = new TestCardContextPopup(testItem.Name);
var result = await Application.Current.MainPage.ShowPopupAsync(popup);
BrowsableCommandType? testCommand = (BrowsableCommandType?) result;
if (testCommand == null) return;
switch (testCommand)
{
case BrowsableCommandType.DELETE:
BrowsingItems.Remove(testItem);
break;
}
}
}
After the deletion, the TestCard is removed and CollectionView refreshes. If I add another TestItem with the button, its added with the appropriate name displayed.
However, if I hold on it, the name displayed on the popup is of the previous TestItem.
it also passes the same previous reference to the ShowTestContextPopup Command.
Any ideas on how to stop it from recycling old references, or how to update the Bindings on the DataTemplate?
EDIT 1:
here is TestItem.cs
public class TestItem : IBaseItem
{
public int ID { get; init; }
public string Name { get; init; }
public int QuestionsAmount { get; init; }
public int QuestionsAnswered { get; init; }
public bool Random { get; init; }
public double PercentageAnswered => (QuestionsAmount==0) ? 0 : (double)QuestionsAnswered / (double)QuestionsAmount;
}
The BrowsingItemTemplateSelector
public class BrowsingItemDataTemplateSelector : DataTemplateSelector
{
public DataTemplate? TemplateTest { get; set; }
public DataTemplate? TemplateFolder { get; set; }
protected override DataTemplate? OnSelectTemplate(object item, BindableObject container)
{
return ((IBaseItem)item is TestItem) ? TemplateTest : TemplateFolder;
}
}
TestCard.xaml
<Border xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:vm="clr-namespace:TryMe.Resources.Models"
x:Class="TryMe.Resources.Controls.TestCard"
x:DataType="vm:TestItem"
Padding="5"
StrokeThickness="3"
BackgroundColor="{StaticResource AppSecondary}"
HeightRequest="80"
Margin="12">
<Border.StrokeShape>
<RoundRectangle CornerRadius="15"></RoundRectangle>
</Border.StrokeShape>
<Grid RowDefinitions="*,*">
<Image Source="test_black.svg" HeightRequest="35" WidthRequest="35"
Aspect="AspectFit"
HorizontalOptions="Start"
VerticalOptions="Center"></Image>
<Label Text="{Binding Name}" x:Name="nameLbl"
HorizontalOptions="Center"
VerticalOptions="Center"
FontSize="Medium"></Label>
<Label Text="{Binding QuestionsAmount}"
Grid.Row="1"
HorizontalOptions="Start"
VerticalOptions="Center"
HorizontalTextAlignment="Center"
VerticalTextAlignment="Center"
FontSize="Medium"
HeightRequest="35"
WidthRequest="35">
</Label>
<ProgressBar Margin="50,0,10,0" Grid.Row="1" Progress="{Binding PercentageAnswered}" HeightRequest="25" ProgressColor="Black" BackgroundColor="{StaticResource AppSecondary}"></ProgressBar>
</Grid>
</Border>
TestCard.xaml.cs
is just the constructor with InitializeComponent().
The ShowTestContextPopup.xaml:
<toolkit:Popup xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
xmlns:controls="clr-namespace:TryMe.Resources.Controls"
x:Class="TryMe.Resources.Controls.Popups.TestCardContextPopup" Color="Transparent">
<Border Stroke="{StaticResource AppGold}"
StrokeThickness="5"
WidthRequest="250"
HeightRequest="150"
StrokeShape="RoundRectangle 15"
BackgroundColor="{StaticResource AppPrimary}"
Padding="5">
<Grid BackgroundColor="{StaticResource AppPrimary}"
VerticalOptions="Center"
HorizontalOptions="Center"
RowDefinitions="45,50"
ColumnDefinitions="50,50,50,50"
ColumnSpacing="5"
RowSpacing="5"
>
<Label
x:Name="nameLbl"
TextColor="{StaticResource AppGold}"
Grid.Row="0"
Grid.ColumnSpan="6"
FontSize="26"
HorizontalOptions="Center"
HorizontalTextAlignment="Center"
VerticalTextAlignment="Center"></Label>
<controls:GoldMenuButton Image="edit_gold.svg"
ImagePadding="8"
Grid.Row="1"
Grid.Column="0"
x:Name="editBttn"
/>
<controls:GoldMenuButton Image="info_gold.svg"
Grid.Row="1"
Grid.Column="1"
x:Name="infoBttn"
/>
<controls:GoldMenuButton Image="trash_gold.svg"
Grid.Row="1"
Grid.Column="2"
x:Name="deleteBttn" />
<controls:GoldMenuButton Image="reset_gold.svg"
Grid.Row="1"
Grid.Column="3"
x:Name="resetBttn" />
</Grid>
</Border>
</toolkit:Popup>
public enum BrowsableCommandType { EDIT, INFO, DELETE, RESET };
public partial class TestCardContextPopup : Popup
{
public TestCardContextPopup(string name)
{
InitializeComponent();
nameLbl.Text = name;
editBttn.Clicked = new Command (() => { ReturnTestCommand(BrowsableCommandType.EDIT); });
infoBttn.Clicked = new Command(() => { ReturnTestCommand(BrowsableCommandType.INFO); });
deleteBttn.Clicked = new Command(() => { ReturnTestCommand(BrowsableCommandType.DELETE); });
resetBttn.Clicked = new Command(() => { ReturnTestCommand(BrowsableCommandType.RESET); });
}
public void ReturnTestCommand(BrowsableCommandType command)
{
Close(command);
}
}
IBaseItem :
public interface IBaseItem
{
public int ID { get;}
}
GoldMenuAddButton:
<Border xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="TryMe.Resources.Controls.GoldMenuAddButton"
Stroke="{StaticResource AppGold}"
BackgroundColor="{StaticResource AppDarkPrimary}"
Padding="5"
StrokeThickness="3">
<Border.StrokeShape>
<RoundRectangle CornerRadius="15"></RoundRectangle>
</Border.StrokeShape>
<Border.GestureRecognizers>
<TapGestureRecognizer x:Name="tapRecognizer"
NumberOfTapsRequired="1" />
</Border.GestureRecognizers>
<Grid ColumnDefinitions="*,*" ColumnSpacing="25" Padding="1" >
<Image x:Name="image" WidthRequest="45"
Aspect="AspectFit"
HorizontalOptions="Center"
VerticalOptions="Center"
Grid.Column="0" >
</Image>
<Image
Aspect="AspectFit"
Source="add_gold.svg"
HorizontalOptions="Center"
VerticalOptions="Center"
Grid.Column="1">
</Image>
</Grid>
</Border>
Code behind:
public partial class GoldMenuAddButton : Border
{
public GoldMenuAddButton()
{
InitializeComponent();
}
public BindableProperty ImageProperty = BindableProperty.Create(nameof(Image), typeof(ImageSource), typeof(GoldMenuAddButton)
, propertyChanged: (bindable, oldValue, newValue) =>
{
var control = bindable as GoldMenuAddButton;
control.image.Source = newValue as ImageSource;
});
public static readonly BindableProperty ClickedProperty = BindableProperty.Create(nameof(Clicked), typeof(ICommand), typeof(GoldMenuAddButton)
, propertyChanged: (bindable, oldValue, newValue) =>
{
var control = bindable as GoldMenuAddButton;
control.tapRecognizer.Command = newValue as ICommand; ;
});
public ICommand Clicked
{
get => GetValue(ClickedProperty) as ICommand;
set => SetValue(ClickedProperty, value);
}
public ImageSource Image
{
get => GetValue(ImageProperty) as ImageSource;
set => SetValue(ImageProperty, value);
}
}
STEPS TO REPRODUCE: v1)
The problem is due to how behaviours handle binding, the same problem doesn't seem to occur when using regular GestureRecognizer. Apparently the creator of this feature regrets it to this day.