Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to set a Binding from ItemTemplate to the hosting Container in an ItemsControl? (UWP)

Tags:

c#

.net

wpf

xaml

uwp

Given an arbitrary ItemsControl, like a ListView, I want to set a Binding from inside the ItemsTemplate to the hosting Container. How can I do that easily? For example, in WPF we can do it 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) the Binding will be set between Property in SomeControl and TargetProperty of the ListViewItem (implicit, because it will be generated dynamically by the ListView to host the each of its items).

How can we do achieve the same in UWP?

I want something that is MVVM-friendly. Maybe with attached properties or an Interaction Behavior.

like image 530
SuperJMN Avatar asked Dec 20 '16 12:12

SuperJMN


3 Answers

When the selection changes, search the visual tree for the radio button with the DataContext corresponding to selected/deselected items. Once it's found, you can check/uncheck at your leisure.

I have a toy model object looking 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" },
    };

The list view, binding to the above collection:

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

The 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;
    }
}

Finding the radio button with a DataContext matching 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 to find a child of a specific 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);
}

The result:

enter image description here

This will break if a given model instance shows up more than once in the list.

like image 91
Petter Hesselberg Avatar answered Sep 21 '22 02:09

Petter Hesselberg


Note: this answer is based on WPF, there might be some changes necessary for UWP.

There are basically two cases to consider:

  1. You have a data driven aspect that needs to be bound to the item container
  2. You have a view-only property

Lets assume a customized listview 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 the ItemContainerStyle to link your viewmodel property with a 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 appears that you don't really have a data driven property and consequently, the effects of your property should be reflected within the control (ControlTemplate).

In the following example a toolbar is made visible based on the IsEditing property. A togglebutton can be used to control the property, the ItemTemplate is used as an inner element next to the toolbar and button, it knows nothing of the IsEditing state:

<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 may chose to use Blend and create the control template starting at the full ListViewItem template and just editing your changes into it.

If your DesignerItem generally has a specific enhanced appearance, consider designing it in the Themes/Generic.xaml with the appropriate default style.


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

In order to apply the template without the need for Setter.Value bindings, you can use value coercion on 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, for an easier example I will activate the "edit" mode by ListViewItem.IsSelected with 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 edit-enabled, getting the local:MyListView.ItemEditTemplate (green) instead of the default template (red)

like image 37
grek40 Avatar answered Sep 20 '22 02:09

grek40


Just in case you might want to have an IsSelected property in your view model item class, you may create a derived ListView that establishes a 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
            });
    }
}

You might now simply bind the RadioButton's IsChecked property in the ListView's 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 Content property. Obviously, the IsSelected property of the data item class must fire a PropertyChanged event.

like image 22
Clemens Avatar answered Sep 23 '22 02:09

Clemens