Occurs iOS/Android:
In App.xaml I've created a DataTemplate for a ListViewItem that is selected with a DataTemplateSelector. Nothing new here.
The purpose of the list is to allow the user to monitor the state of uploads happening in the background, hence the control has a status of one of four states:
public enum UploadingStatus
{
None, // Ignore this!
Uploading,
Pending,
Complete,
Failed
}
The buggy DataTemplate in question, wraps a custom control which extends ViewCell, and this custom control has a Status bindableproperty and depending upon which of it's 4 enumerated values it is bound to, depends upon, when the 'Status' propertychanged event fires, which collection of MenuItem it assigns to the ViewCell.ContextActions property.
_
The reason it does this, is depending which of the 4 Status modes it is in, depends upon which collection of MenuItems appears when the user 'swipes left' (ios) or 'long presses' on Android._
The extraordinary thing, is that although I assign only 4 dummy MenuItems to each of the MenuItem collections (see the Xaml below), when the Status propertychanged fires, there are 4 items in UploadingMenuItems, 8 items in FailedMenuItems, 12 items in PendingMenuItems, and 16 items in CompletedMenuItems.
I should add, the four mentioned above are as assigned in the Xaml, the 8 mentioned above are twice the FailedMenuItems MenuItem assignment in the Xaml, the 12 mentioned above are thrice the PendingMenuItems in the Xaml, and the 16 mentioned above are quad the CompletedMenuItems assigned in the Xaml ... all only show the ones I declared in their particular property, and this I know because when looking at the UI on iOS, the menu item labels illustrated prove where they were declared.
And yet as you can see in the Xaml below, I only assign 4 MenuItem objects to each of UploadingMenuItems, PendingMenuItems, CompletedMenuItems, FailedMenuItems And yet multiples of this end up in each collection.
Curiously, the order of the assignments above is not in the order of the properties declared in Xaml below, but in the order that they are declared in my mock data collection that the ListView is bound to:
var itemSource = new ObservableCollection<Upload>
{
new Upload { UploadType = UploadingTemplateType.Separator },
new Upload { UploadType = UploadingTemplateType.Uploading, DisplayData = new UploadItem { DocumentId = Guid.NewGuid().ToString(), DocumentTitle = "Document 1", Message = "Uploading Media", ContentRegion = 54, Status = UploadingStatus.Uploading }},
new Upload { UploadType = UploadingTemplateType.Separator },
new Upload { UploadType = UploadingTemplateType.Failed, DisplayData = new UploadItem { DocumentId = Guid.NewGuid().ToString(), DocumentTitle = "Document 2", Message = "Upload failed because of Network failure", Status = UploadingStatus.Failed }},
new Upload { UploadType = UploadingTemplateType.Separator },
new Upload { UploadType = UploadingTemplateType.Pending, DisplayData = new UploadItem { DocumentId = Guid.NewGuid().ToString(), DocumentTitle = "Document 3", Message = "Pending Upload", Status = UploadingStatus.Pending }},
new Upload { UploadType = UploadingTemplateType.Separator },
new Upload { UploadType = UploadingTemplateType.Complete, DisplayData = new UploadItem { DocumentId = Guid.NewGuid().ToString(), DocumentTitle = "Document 4", Message = "Upload Complete", Status = UploadingStatus.Complete }},
new Upload { UploadType = UploadingTemplateType.Separator }
};
ItemsSource = itemSource;
The applicable data declaration in the above list is every second line (the DataTemplateSelector selects a different template for a custom ListViewItem separator every first item, and the following template for every data line):
<DataTemplate x:Key="UploadsListViewItemTemplate">
<controls:UploadsListViewItem
Title="{Binding DisplayData.DocumentTitle}"
Message="{Binding DisplayData.Message}"
Icon="{Binding DisplayData.Status, Converter={StaticResource UploadsStatusToIconConverter}}"
IconColor="{Binding DisplayData.Status, Converter={StaticResource UploadsStatusToIconColorConverter}}"
IconFontSize="35"
ContentRegion="{Binding DisplayData.ContentRegion}"
IsActive="{Binding DisplayData.IsActive}"
Status="{Binding DisplayData.Status}"
>
<controls:UploadsListViewItem.UploadingMenuItems>
<MenuItem Clicked="UploadsDemoItem" CommandParameter="{Binding DisplayData.DocumentId}" Text="Demo1 U" />
<MenuItem Clicked="UploadsDemoItem" CommandParameter="{Binding DisplayData.DocumentId}" Text="Demo2 U" />
<MenuItem Clicked="UploadsDemoItem" CommandParameter="{Binding DisplayData.DocumentId}" Text="Demo3 U" />
<MenuItem Clicked="UploadsDemoItem" CommandParameter="{Binding DisplayData.DocumentId}" Text="Demo4 U" />
</controls:UploadsListViewItem.UploadingMenuItems>
<controls:UploadsListViewItem.PendingMenuItems>
<MenuItem Clicked="UploadsDemoItem" CommandParameter="{Binding DisplayData.DocumentId}" Text="Demo1 P" />
<MenuItem Clicked="UploadsDemoItem" CommandParameter="{Binding DisplayData.DocumentId}" Text="Demo2 P" />
<MenuItem Clicked="UploadsDemoItem" CommandParameter="{Binding DisplayData.DocumentId}" Text="Demo3 P" />
<MenuItem Clicked="UploadsDemoItem" CommandParameter="{Binding DisplayData.DocumentId}" Text="Demo4 P" />
</controls:UploadsListViewItem.PendingMenuItems>
<controls:UploadsListViewItem.CompletedMenuItems>
<MenuItem Clicked="UploadsDemoItem" CommandParameter="{Binding DisplayData.DocumentId}" Text="Demo1 C" />
<MenuItem Clicked="UploadsDemoItem" CommandParameter="{Binding DisplayData.DocumentId}" Text="Demo2 C" />
<MenuItem Clicked="UploadsDemoItem" CommandParameter="{Binding DisplayData.DocumentId}" Text="Demo3 C" />
<MenuItem Clicked="UploadsDemoItem" CommandParameter="{Binding DisplayData.DocumentId}" Text="Demo4 C" />
</controls:UploadsListViewItem.CompletedMenuItems>
<controls:UploadsListViewItem.FailedMenuItems>
<MenuItem Clicked="UploadsDemoItem" CommandParameter="{Binding DisplayData.DocumentId}" Text="Demo1 F" />
<MenuItem Clicked="UploadsDemoItem" CommandParameter="{Binding DisplayData.DocumentId}" Text="Demo2 F" />
<MenuItem Clicked="UploadsDemoItem" CommandParameter="{Binding DisplayData.DocumentId}" Text="Demo3 F" />
<MenuItem Clicked="UploadsDemoItem" CommandParameter="{Binding DisplayData.DocumentId}" Text="Demo4 F" />
</controls:UploadsListViewItem.FailedMenuItems>
</controls:UploadsListViewItem>
</DataTemplate>
My complete Custom Control code behind is as follows (You may as well scroll to the last 5 BindableProperty declarations, UploadingMenuItems, PendingMenuItems, CompletedMenuItems, FailedMenuItems, and Status):
public partial class UploadsListViewItem : ViewCell
{
#region bool IsActive [BindableProperty]
public static readonly BindableProperty IsActiveProperty =
BindableProperty.CreateAttached(
"IsActive",
typeof(bool),
typeof(UploadsListViewItem),
false,
BindingMode.TwoWay);
public bool IsActive
{
get { return (bool)GetValue(IsActiveProperty); }
set { SetValue(IsActiveProperty, value); }
}
#endregion
#region string Icon [BindableProperty]
public static readonly BindableProperty IconProperty =
BindableProperty.CreateAttached(
"Icon",
typeof(string),
typeof(UploadsListViewItem),
Lexacom.Mahon.FormsApp.Controls.Icon.FAThumbTack,
BindingMode.OneWay);
public string Icon
{
get { return (string)GetValue(IconProperty); }
set { SetValue(IconProperty, value); }
}
#endregion
#region Color IconColor [BindableProperty]
public static readonly BindableProperty IconColorProperty =
BindableProperty.CreateAttached(
"IconColor",
typeof(Color),
typeof(UploadsListViewItem),
Color.FromHex("#000000"),
BindingMode.OneWay);
public Color IconColor
{
get { return (Color)GetValue(IconColorProperty); }
set { SetValue(IconColorProperty, value); }
}
#endregion
#region double IconFontSize [BindableProperty]
public static readonly BindableProperty IconFontSizeProperty =
BindableProperty.CreateAttached(
"IconFontSize",
typeof(double),
typeof(UploadsListViewItem),
20.0,
BindingMode.OneWay);
public double IconFontSize
{
get { return (double)GetValue(IconFontSizeProperty); }
set { SetValue(IconFontSizeProperty, value); }
}
#endregion
#region string Title [BindableProperty]
public static readonly BindableProperty TitleProperty =
BindableProperty.CreateAttached(
"Title",
typeof(string),
typeof(UploadsListViewItem),
string.Empty,
BindingMode.OneWay);
public string Title
{
get { return (string)GetValue(TitleProperty); }
set { SetValue(TitleProperty, value); }
}
#endregion
#region string Message [BindableProperty]
public static readonly BindableProperty MessageProperty =
BindableProperty.CreateAttached(
"Message",
typeof(string),
typeof(UploadsListViewItem),
string.Empty,
BindingMode.OneWay);
public string Message
{
get { return (string)GetValue(MessageProperty); }
set { SetValue(MessageProperty, value); }
}
#endregion
#region object ContentRegion [BindableProperty]
public static readonly BindableProperty ContentRegionProperty =
BindableProperty.CreateAttached(
"ContentRegion",
typeof(object),
typeof(UploadsListViewItem),
null,
BindingMode.TwoWay);
public object ContentRegion
{
get { return (object)GetValue(ContentRegionProperty); }
set { SetValue(ContentRegionProperty, value); }
}
#endregion
#region ICommand DemoMenuClickCommand [BindableProperty]
public static readonly BindableProperty DemoMenuClickCommandProperty =
BindableProperty.CreateAttached(
"DemoMenuClickCommand",
typeof(ICommand),
typeof(UploadsListViewItem),
null,
BindingMode.TwoWay);
public ICommand DemoMenuClickCommand
{
get { return (ICommand)GetValue(DemoMenuClickCommandProperty); }
set { SetValue(DemoMenuClickCommandProperty, value); }
}
#endregion
#region List<MenuItem> UploadingMenuItems [BindableProperty]
public static readonly BindableProperty UploadingMenuItemsProperty =
BindableProperty.CreateAttached(
"UploadingMenuItems",
typeof(List<MenuItem>),
typeof(UploadsListViewItem),
new List<MenuItem>(),
BindingMode.OneWay);
public List<MenuItem> UploadingMenuItems
{
get { return (List<MenuItem>)GetValue(UploadingMenuItemsProperty); }
set { SetValue(UploadingMenuItemsProperty, value); }
}
#endregion
#region List<MenuItem> PendingMenuItems [BindableProperty]
public static readonly BindableProperty PendingMenuItemsProperty =
BindableProperty.CreateAttached(
"PendingMenuItems",
typeof(List<MenuItem>),
typeof(UploadsListViewItem),
new List<MenuItem>(),
BindingMode.OneWay);
public List<MenuItem> PendingMenuItems
{
get { return (List<MenuItem>)GetValue(PendingMenuItemsProperty); }
set { SetValue(PendingMenuItemsProperty, value); }
}
#endregion
#region List<MenuItem> CompletedMenuItems [BindableProperty]
public static readonly BindableProperty CompletedMenuItemsProperty =
BindableProperty.CreateAttached(
"CompletedMenuItems",
typeof(List<MenuItem>),
typeof(UploadsListViewItem),
new List<MenuItem>(),
BindingMode.OneWay);
public List<MenuItem> CompletedMenuItems
{
get { return (List<MenuItem>)GetValue(CompletedMenuItemsProperty); }
set { SetValue(CompletedMenuItemsProperty, value); }
}
#endregion
#region List<MenuItem> FailedMenuItems [BindableProperty]
public static readonly BindableProperty FailedMenuItemsProperty =
BindableProperty.CreateAttached(
"FailedMenuItems",
typeof(List<MenuItem>),
typeof(UploadsListViewItem),
new List<MenuItem>(),
BindingMode.OneWay);
public List<MenuItem> FailedMenuItems
{
get { return (List<MenuItem>)GetValue(FailedMenuItemsProperty); }
set { SetValue(FailedMenuItemsProperty, value); }
}
#endregion
#region UploadingStatus Status [BindableProperty]
public static readonly BindableProperty StatusProperty =
BindableProperty.CreateAttached(
"Status",
typeof(UploadingStatus),
typeof(UploadsListViewItem),
UploadingStatus.None,
BindingMode.TwoWay);
public UploadingStatus Status
{
get { return (UploadingStatus)GetValue(StatusProperty); }
set { SetValue(StatusProperty, value); }
}
#endregion
public UploadsListViewItem()
{
InitializeComponent();
IconInfoPanel.BindingContext = this;
this.PropertyChanged += (object sender, System.ComponentModel.PropertyChangedEventArgs e) =>
{
if (e.PropertyName == UploadsListViewItem.StatusProperty.PropertyName)
{
this.ContextActions.Clear();
AssignMenuItems();
}
};
}
private void AssignMenuItems()
{
switch (Status)
{
case UploadingStatus.Complete:
AddMenuItems(CompletedMenuItems);
break;
case UploadingStatus.Failed:
AddMenuItems(FailedMenuItems);
break;
case UploadingStatus.Pending:
AddMenuItems(PendingMenuItems);
break;
case UploadingStatus.Uploading:
AddMenuItems(UploadingMenuItems);
break;
case UploadingStatus.None:
default:
break;
}
}
private void AddMenuItems(List<MenuItem> items)
{
if (items == null || items.Count == 0) return;
foreach(var item in items)
{
this.ContextActions.Add(item);
}
}
}
I'd very much like a resolution here as it is important for me to be able to change the MenuItems in a ListViewItem when a particular bound property changes.
The Xaml for the custom control illustrates that the bindings to the properties in question are not consumed by any other part of the control:
<?xml version="1.0" encoding="UTF-8"?>
<ViewCell
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Redacted.FormsApp.Controls.UploadsListViewItem"
xmlns:controls="clr-namespace:Redacted.FormsApp.Controls;assembly=Redacted.FormsApp"
>
<StackLayout Orientation="Vertical">
<controls:IconInfoPanel
x:Name="IconInfoPanel"
VerticalOptions="Fill"
Title="{Binding Title}"
Message="{Binding Message}"
Icon="{Binding Icon}"
IconColor="{Binding IconColor}"
IconFontSize="{Binding IconFontSize}"
ContentRegion="{Binding ContentRegion}"
IsActive="{Binding IsActive}"
/>
</StackLayout>
</ViewCell>
Any ideas?
I hope that this is not a ListViewItem virtualisation problem!
To add to the problem, if I navigate away from the page, and then back, additional multiples of each item add to the applicable ListViewItem.