How to set binding from ItemTemplate to container for placement in ItemsControl? (UWP)

For an arbitrary ItemsControl , like a ListView , I want to set the binding from within the ItemsTemplate to the storage container. How can i make it easy? For example, in WPF we can do this using this inside the ItemTemplate

 <ListView.ItemTemplate> <DataTemplate> <SomeControl Property="{Binding Path=TargetProperty, RelativeSouce={RelativeSource FindAncestor, AncestorType={x:Type MyContainer}}}" /> </DataTemplate> <ListView.ItemTemplate> 

In this example (for WPF), a binding will be set between Property in SomeControl and TargetProperty ListViewItem (implicit, since it will be generated dynamically using ListView to place each of its elements).

How can we achieve this in UWP?

I want something that is MVVM-friendly. Perhaps with attached properties or interaction.

+6
source share
3 answers

Note: this answer is based on WPF, for UWP some changes may be required.

There are basically two cases:

  • You have a data-driven attribute that needs to be bound to an element container.
  • You have a view-only property.

Let's look at an individual list for both cases:

 public class MyListView : ListView { protected override DependencyObject GetContainerForItemOverride() { return new DesignerItem(); } protected override bool IsItemItsOwnContainerOverride(object item) { return item is DesignerItem; } } public class DesignerItem : ListViewItem { public bool IsEditing { get { return (bool)GetValue(IsEditingProperty); } set { SetValue(IsEditingProperty, value); } } public static readonly DependencyProperty IsEditingProperty = DependencyProperty.Register("IsEditing", typeof(bool), typeof(DesignerItem)); } 

In case 1, you can use ItemContainerStyle to bind the viewmodel property to the binding, and then bind the same property inside the datatemplate

 class MyData { public bool IsEditing { get; set; } // also need to implement INotifyPropertyChanged here! } 

XAML:

 <local:MyListView ItemsSource="{Binding Source={StaticResource items}}"> <local:MyListView.ItemContainerStyle> <Style TargetType="{x:Type local:DesignerItem}"> <Setter Property="IsEditing" Value="{Binding IsEditing,Mode=TwoWay}"/> <Setter Property="HorizontalContentAlignment" Value="Stretch"/> </Style> </local:MyListView.ItemContainerStyle> <local:MyListView.ItemTemplate> <DataTemplate> <Border Background="Red" Margin="5" Padding="5"> <CheckBox IsChecked="{Binding IsEditing}"/> </Border> </DataTemplate> </local:MyListView.ItemTemplate> </local:MyListView> 

In case 2, it looks like you really do not have a data-driven property, and therefore the effects of your property should be reflected inside the ControlTemplate .

In the following example, the toolbar becomes visible based on the IsEditing property. To control this property, you can use the toggle switch, ItemTemplate used as an internal element next to the toolbar and button, it does not know anything about the status of IsEditing :

 <local:MyListView ItemsSource="{Binding Source={StaticResource items}}"> <local:MyListView.ItemContainerStyle> <Style TargetType="{x:Type local:DesignerItem}"> <Setter Property="IsEditing" Value="{Binding IsEditing,Mode=TwoWay}"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type local:DesignerItem}"> <DockPanel> <ToggleButton DockPanel.Dock="Right" Margin="5" VerticalAlignment="Top" IsChecked="{Binding IsEditing,RelativeSource={RelativeSource TemplatedParent},Mode=TwoWay}" Content="Edit"/> <!--Toolbar is something control related, rather than data related--> <ToolBar x:Name="MyToolBar" DockPanel.Dock="Top" Visibility="Collapsed"> <Button Content="Tool"/> </ToolBar> <ContentPresenter ContentSource="Content"/> </DockPanel> <ControlTemplate.Triggers> <Trigger Property="IsEditing" Value="True"> <Setter TargetName="MyToolBar" Property="Visibility" Value="Visible"/> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style> </local:MyListView.ItemContainerStyle> <local:MyListView.ItemTemplate> <DataTemplate> <Border Background="Red" Margin="5" Padding="5"> <TextBlock Text="Hello World"/> </Border> </DataTemplate> </local:MyListView.ItemTemplate> </local:MyListView> 

For a better control template, you can use Blend and create a control template, starting with the full ListViewItem template and simply edit its changes.

If your DesignerItem usually has a special enhanced look, consider designing it in Themes/Generic.xaml with the appropriate default style.


As noted, you can provide a separate data template for editing mode. To do this, add the MyListView and DesignerItem properties and use MyListView.PrepareContainerForItemOverride(...) to transfer the template.

To apply a template without the Setter.Value binding, you can use coercion of the value on the DesignerItem.ContentTemplate based on IsEditing .

 public class MyListView : ListView { protected override DependencyObject GetContainerForItemOverride() { return new DesignerItem(); } protected override bool IsItemItsOwnContainerOverride(object item) { return item is DesignerItem; } protected override void PrepareContainerForItemOverride(DependencyObject element, object item) { base.PrepareContainerForItemOverride(element, item); var elem = element as DesignerItem; elem.ContentEditTemplate = ItemEditTemplate; } public DataTemplate ItemEditTemplate { get { return (DataTemplate)GetValue(ItemEditTemplateProperty); } set { SetValue(ItemEditTemplateProperty, value); } } public static readonly DependencyProperty ItemEditTemplateProperty = DependencyProperty.Register("ItemEditTemplate", typeof(DataTemplate), typeof(MyListView)); } public class DesignerItem : ListViewItem { static DesignerItem() { ContentTemplateProperty.OverrideMetadata(typeof(DesignerItem), new FrameworkPropertyMetadata( null, new CoerceValueCallback(CoerceContentTemplate))); } private static object CoerceContentTemplate(DependencyObject d, object baseValue) { var self = d as DesignerItem; if (self != null && self.IsEditing) { return self.ContentEditTemplate; } return baseValue; } private static void OnIsEditingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { d.CoerceValue(ContentTemplateProperty); } public bool IsEditing { get { return (bool)GetValue(IsEditingProperty); } set { SetValue(IsEditingProperty, value); } } public static readonly DependencyProperty IsEditingProperty = DependencyProperty.Register("IsEditing", typeof(bool), typeof(DesignerItem), new FrameworkPropertyMetadata(new PropertyChangedCallback(OnIsEditingChanged))); public DataTemplate ContentEditTemplate { get { return (DataTemplate)GetValue(ContentEditTemplateProperty); } set { SetValue(ContentEditTemplateProperty, value); } } // Using a DependencyProperty as the backing store for ContentEditTemplate. This enables animation, styling, binding, etc... public static readonly DependencyProperty ContentEditTemplateProperty = DependencyProperty.Register("ContentEditTemplate", typeof(DataTemplate), typeof(DesignerItem)); } 

Note that for an easier example, I will activate edit mode using ListViewItem.IsSelected using some trigger:

 <local:MyListView ItemsSource="{Binding Source={StaticResource items}}"> <local:MyListView.ItemContainerStyle> <Style TargetType="{x:Type local:DesignerItem}"> <Style.Triggers> <Trigger Property="IsSelected" Value="True"> <Setter Property="IsEditing" Value="True"/> </Trigger> </Style.Triggers> </Style> </local:MyListView.ItemContainerStyle> <local:MyListView.ItemTemplate> <DataTemplate> <Border Background="Red" Margin="5" Padding="5"> <TextBlock Text="Hello World"/> </Border> </DataTemplate> </local:MyListView.ItemTemplate> <local:MyListView.ItemEditTemplate> <DataTemplate> <Border Background="Green" Margin="5" Padding="5"> <TextBlock Text="Hello World"/> </Border> </DataTemplate> </local:MyListView.ItemEditTemplate> </local:MyListView> 

Intended behavior: the selected item becomes enabled for editing, getting local:MyListView.ItemEditTemplate (green) instead of the default template (red)

+1
source

When the selection changes, search the visualization tree for the switch using the DataContext corresponding to the selected / deselected items. Once it is found, you can check / uncheck the box in your free time.

I have a toy model object that looks like this:

 public class Data { public string Name { get; set; } } 

My Page is named self and contains this collection property:

 public Data[] Data { get; set; } = { new Data { Name = "One" }, new Data { Name = "Two" }, new Data { Name = "Three" }, }; 

List view associated with the above collection:

 <ListView ItemsSource="{Binding Data, ElementName=self}" SelectionChanged="OnSelectionChanged"> <ListView.ItemTemplate> <DataTemplate> <RadioButton Content="{Binding Name}" /> </DataTemplate> </ListView.ItemTemplate> </ListView> 

SelectionChanged event handler:

 private void OnSelectionChanged(object sender, SelectionChangedEventArgs e) { ListView lv = sender as ListView; var removed = FindRadioButtonWithDataContext(lv, e.RemovedItems.FirstOrDefault()); if (removed != null) { removed.IsChecked = false; } var added = FindRadioButtonWithDataContext(lv, e.AddedItems.FirstOrDefault()); if (added != null) { added.IsChecked = true; } } 

Search for a switch with a DataContext corresponding to our Data instance:

 public static RadioButton FindRadioButtonWithDataContext( DependencyObject parent, object data) { if (parent != null) { int childrenCount = VisualTreeHelper.GetChildrenCount(parent); for (int i = 0; i < childrenCount; i++) { DependencyObject child = VisualTreeHelper.GetChild(parent, i); ListViewItem lv = child as ListViewItem; if (lv != null) { RadioButton rb = FindVisualChild<RadioButton>(child); if (rb?.DataContext == data) { return rb; } } RadioButton childOfChild = FindRadioButtonWithDataContext(child, data); if (childOfChild != null) { return childOfChild; } } } return null; } 

And finally, a helper method for finding a child of a certain type:

 public static T FindVisualChild<T>( DependencyObject parent) where T : DependencyObject { if (parent != null) { int childrenCount = VisualTreeHelper.GetChildrenCount(parent); for (int i = 0; i < childrenCount; i++) { DependencyObject child = VisualTreeHelper.GetChild(parent, i); T candidate = child as T; if (candidate != null) { return candidate; } T childOfChild = FindVisualChild<T>(child); if (childOfChild != null) { return childOfChild; } } } return default(T); } 

Result:

enter image description here

This will cause a breakdown if an instance of this model is displayed more than once in the list.

+2
source

Just in case, you might want to have the IsSelected property in the class of the view model elements, you can create a derived ListView that sets the binding of its ListViewItems to the view model property:

 public class MyListView : ListView { public string ItemIsSelectedPropertyName { get; set; } = "IsSelected"; protected override void PrepareContainerForItemOverride( DependencyObject element, object item) { base.PrepareContainerForItemOverride(element, item); BindingOperations.SetBinding(element, ListViewItem.IsSelectedProperty, new Binding { Path = new PropertyPath(ItemIsSelectedPropertyName), Source = item, Mode = BindingMode.TwoWay }); } } 

Now you can simply bind the RadioButton IsChecked property in the ListView ItemTemplate to the same view model property:

 <local:MyListView ItemsSource="{Binding DataItems}"> <ListView.ItemTemplate> <DataTemplate> <RadioButton Content="{Binding Content}" IsChecked="{Binding IsSelected, Mode=TwoWay}"/> </DataTemplate> </ListView.ItemTemplate> </local:MyListView> 

In the above example, the data item class also has the Content property. Obviously, the IsSelected property of the IsSelected class should fire the PropertyChanged event.

+1
source

Source: https://habr.com/ru/post/1013333/


All Articles