6

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.

SuperJMN
  • 13,110
  • 16
  • 86
  • 185
  • http://stackoverflow.com/questions/32861612/how-to-do-relativesource-mode-find-ancestor-or-equivalent-in-uwp – WPFUser Dec 20 '16 at 12:35
  • @WPFUser Sorry, but I don't see how that could be applied to my needs. – SuperJMN Dec 20 '16 at 12:44
  • 1
    Well, the problem is that the ListViewItem is created implicitly, and not easy to get at. If you have any ideas on that score I'd be interested. – Petter Hesselberg Dec 20 '16 at 15:15
  • I wonder if we could create a derived ListView class in which you can include the ListViewItem in the DataTemplate, so it won't generate a new container, but use THAT explicit ListViewItem. The a Binding with ElementName could be used. Am I too imaginative? – SuperJMN Dec 20 '16 at 15:18
  • Off the top of my head, I think that might be troublesome. If you add a `ListViewItem` to the `DataTemplate`, you end up with a `ListViewItem` (yours) wrapped in a `ListViewItemPresenter` wrapped in yet another `ListViewItem`... – Petter Hesselberg Dec 20 '16 at 15:22
  • Couldn't that be handled inside the derived ListView so we can avoid duplicating containers? – SuperJMN Dec 20 '16 at 15:23
  • Possibly. If you try to go that route I'd be interested in the results. (Which might end up as another answer to this question.) – Petter Hesselberg Dec 20 '16 at 16:48
  • 1
    Are you attempting to bind within the DataTemplate to something that is outside? Trying to understand if the question is around binding to main model properties from within ItemTemplate. – loopedcode Nov 29 '17 at 04:45
  • Yes, exactly. For example, in a ListView, to bind a property of from inside the template to the containing ListiewItem. Sorry, the title is wrong – SuperJMN Nov 29 '17 at 07:37
  • What is it that you are trying to achieve exactly? – Naweed Akram Nov 29 '17 at 07:41
  • I have just edited the original post to clarify it a bit. I want to bind a control inside the ItemTemplate to the container that is generated automatically to host the items. I would like something that is reusable and MVVM-friendly. – SuperJMN Nov 29 '17 at 08:11
  • Please clarify the reasoning behind this requirement(e.g. the exact property you would like to bind). If it's not possible in UWP it usually is bad practice. – Novitchi S Nov 29 '17 at 08:46
  • All use cases I could think of would be handled by adding an `ItemContainerStyle` in addition to the `ItemTemplate`, so it would really help if you explain an example use case where you feel that you need the requested functionality. – grek40 Nov 29 '17 at 09:45
  • To all: I have a ItemsControl-derived class that generates a special kind of Container, called DesignerItem. This container has a property "IsEditing" which is a state controlled by itself. When the IsEditing property is true, I want to show make a Toolbar visible. This toolbar is INSIDE the ItemTemplate. So I have to bind the Visibility property of the Toobar to the IsEditing property of the DesignerItem (the Container). – SuperJMN Nov 29 '17 at 13:24
  • So in short, either your viewmodel knows about `IsEditing` or you don't have a `DataTemplate` at all, because (as the name implies) a DataTemplate is meant as a way to display a certain set of data and not as a way to interact with its parent containers. – grek40 Nov 29 '17 at 13:39
  • @grek40 My ViewModel doesn't have to know about the changes in the IsEditing property, but the items inside the template should. The items inside DataTemplate should act upon changes in this property. The problem comes from the fact that the IsEditing is exclusive responsibility of the Container. – SuperJMN Nov 29 '17 at 14:24
  • @grek40 So, in short, when the Container decides it's time in editing mode (IsEditing=true), the toolbar inside the ItemTemplate should handle the change. Otherwise I would have to pass the responsibility of handing the user interaction to the ViewModel, that in my case, isn't aware and shouldn't be aware of the state of the container. – SuperJMN Nov 29 '17 at 14:27
  • @grek40 It's entirely a concert of the views. In my scenario, when the user double taps a container, it enters in "EditMode". In such state, the toolbar (defined inside the ItemTemplate) should be shown. But since the IsEditing property is defined into the container, I need a way to link those properties (Visibility and IsEditing). – SuperJMN Nov 29 '17 at 14:34

3 Answers3

2

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.

Petter Hesselberg
  • 5,062
  • 2
  • 24
  • 42
  • That is a nice way to achieve the same result, but it has some drawbacks: it's not reusable, adds a lot of boilerplate, and it looks like a hack. Why did they have to remove the best features of WPF? – SuperJMN Dec 20 '16 at 15:14
  • 1
    Heh. My own favorite missing piece from WPF is custom markup extensions. I'm sure it's possible to package my solution in a more palatable and reusable way, though; I just went the quickest way from A to B. – Petter Hesselberg Dec 20 '16 at 15:17
  • 1
    My last helper method, for example, is a utility I use quite often. – Petter Hesselberg Dec 20 '16 at 15:19
  • Cool! I have just asked about creating custom MarkupExtensions in UWP (http://stackoverflow.com/questions/41245320/custom-markupextension-in-uwp). INCREDIBLE Bummer. – SuperJMN Dec 20 '16 at 15:19
  • 1
    I just amended the code to get rid of the model class dependency. A slight improvement towards reusability. – Petter Hesselberg Dec 20 '16 at 15:35
  • Sorry, but after a year of the OP, I have decided to remove the accepted answer check because this kind of solution is far from being MVVM friendly and is valid only for this scenario (selection of items). If I want to bind to an arbitrary property of the container, you're in trouble again. – SuperJMN Nov 27 '17 at 23:04
1

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.

Clemens
  • 123,504
  • 12
  • 155
  • 268
1

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)

grek40
  • 13,113
  • 1
  • 24
  • 50
  • 1
    Ofcourse, with a custom itemscontrol and itemcontainer, there is also the possibility to just have more templates in the itemscontrol like an additional `local:MyListView.ItemEditTemplate` and make the `ItemContainer` swap between templates for editing. – grek40 Nov 29 '17 at 14:32
  • I'm interested in what you say. How would I swap between the regular and editing templates? I think you cannot just change the ItemTemplate (hotswapping). Do you? – SuperJMN Nov 29 '17 at 14:38
  • By the way, I think you cannot put a Binding as a Value in a Setter in UWP :( https://stackoverflow.com/questions/33573929/uwp-binding-in-style-setter-not-working – SuperJMN Nov 29 '17 at 14:39
  • @SuperJMN I'm not sure, but it's just talking about styles, so with some huge amount of luck, the `ControlTemplate.Triggers-Setter.Value` binding will still work :) – grek40 Nov 29 '17 at 15:07
  • @SuperJMN haha :) see my edit for a different way to handle the active content template (I reworked the part below the horizontal line in the answer). – grek40 Nov 29 '17 at 15:58