Get DataTemplate from data object in ListBox
Asked Answered
S

2

6

I have a ListBox whose ItemTemplate looks like this:

<DataTemplate DataType="local:Column">
    <utils:EditableTextBlock x:Name="editableTextBlock" Text="{Binding Name, Mode=TwoWay}"/>
</DataTemplate>

Column is a simple class which looks like this:

public Column(string name, bool isVisibleInTable)
{
    Name = name;
    IsVisibleInTable = isVisibleInTable;
}

public string Name { get; set; }
public bool IsVisibleInTable { get; set; }

The EditableTextBlock is a UserControl that turns into a TextBox when double clicked and turns back into a TextBlock when Lost Focus. It also has a Property called IsInEditMode which is by default false. When it is true, TextBox is shown.

The Question:
The ItemsSouce of the ListBox is an ObservableCollection<Column>. I have a button which adds new Columns to the collection. But my problem is that I want IsInEditMode to be turned true for newly added EditableTextBlocks by that Button. But I can only access Column in the ViewModel. How will I access the EditableTextBlock of the specified Column in the ItemsSource collection?

The only solution I can come up with is deriving a class from Column and adding a property for that (eg: name: IsInEditMode) (Or maybe a wrapper class. Here's a similar answer which suggestes using a wrapper class) and Binding to that property in the DataTemplate like so:

<DataTemplate DataType="local:DerivedColumn">
    <utils:EditableTextBlock x:Name="editableTextBlock" Text="{Binding Name, Mode=TwoWay}"
                             IsInEditMode="{Binding IsInEditMode}"/>
</DataTemplate>

But I don't want this. I want some way to do this in XAML without deriving classes and adding unnecessary code. (And also adhering to MVVM rules)

Stopped answered 4/1, 2016 at 13:13 Comment(3)
Why not just add the property in the Column class, bind it to IsInEditMode DP and make a ctor with an optionnal parameter to set the mode? You would only have to create a Column and pass true to the ctor in the Command behind the buttonKinetic
I know. That is what I did using the derived class. But I will be using this Column class in other places as well . And in those places, IsInEditMode property will be useless. Is there any XAML only solution?Stopped
Well, I just did a wrapper class instead of deriving it.Stopped
C
2

If you have scope to add a new dependency property to the EditableTextBlock user control you could consider adding one that has the name StartupInEditMode, defaulting to false to keep the existing behavior.

The Loaded handler for the UserControl could then determine the status of StartupInEditMode to decide how to initially set the value of IsInEditMode.

//..... Added to EditableTextBlock user control
    public bool StartupInEdit
    {
        get { return (bool)GetValue(StartupInEditProperty); }
        set { SetValue(StartupInEditProperty, value); }
    }

    public static readonly DependencyProperty StartupInEditProperty = 
        DependencyProperty.Register("StartupInEdit", typeof(bool), typeof(EditableTextBlock ), new PropertyMetadata(false));

    private void EditableTextBlock_OnLoaded(object sender, RoutedEventArgs e)
    {
        IsInEditMode = StartupInEditMode;
    }

For controls already in the visual tree the changing value of StartupInEdit does not matter as it is only evaluated once on creation. This means you can populate the collection of the ListBox where each EditableTextBlock is not in edit mode, then swap the StartupInEditmMode mode to True when you start adding new items. Then each new EditableTextBlock control starts in the edit mode.

You could accomplish this switch in behavior by specifying a DataTemplate where the Binding of this new property points to a variable of the view and not the collection items.

    <DataTemplate DataType="local:Column">
      <utils:EditableTextBlock x:Name="editableTextBlock"
            Text="{Binding Name, Mode=TwoWay}" 
            StartupInEditMode="{Binding ANewViewProperty, RelativeSource={RelativeSource AncestorType={x:Type Window}}}"/>
    </DataTemplate>

You need to add a property to the parent Window (or Page or whatever is used as the containter for the view) called ANewViewProperty in this example. This value could be part of your view model if you alter the binding to {Binding DataContext.ANewViewProperty, RelativeSource={RelativeSource AncestorType={x:Type Window}}}.

This new property (ANewViewProperty) does not even need to implement INotifyPropertyChanged as the binding will get the initial value as it is creating the new EditableTextBlock control and if the value changes later it has no impact anyway.

You would set the value of ANewViewProperty to False as you load up the ListBox ItemSource initially. When you press the button to add a new item to the list set the value of ANewViewProperty to True meaning the control that will now be created starting up in edit mode.

Update: The C#-only, View-only alternative

The code-only, view-only alternative (similar to user2946329's answer)is to hook to the ListBox.ItemContainerGenerator.ItemsChanged handler that will trigger when a new item is added. Once triggered and you are now acting on new items (via Boolean DetectingNewItems) which finds the first descendant EditableTextBlock control for the appropriate ListBoxItem visual container for the item newly added. Once you have a reference for the control, alter the IsInEditMode property.

//.... View/Window Class

    private void MainWindow_OnLoaded(object sender, RoutedEventArgs e)
    {
      MyListBox.ItemContainerGenerator.ItemsChanged += ItemContainerGenerator_ItemsChanged;
    }

    private void ItemContainerGenerator_ItemsChanged(object sender, System.Windows.Controls.Primitives.ItemsChangedEventArgs e)
    {
      if ((e.Action == NotifyCollectionChangedAction.Add) && DetectingNewItems)
      {
        var listboxitem = LB.ItemContainerGenerator.ContainerFromIndex(e.Position.Index + 1) as ListBoxItem;

        var editControl = FindFirstDescendantChildOf<EditableTextBlock>(listboxitem);
        if (editcontrol != null) editcontrol.IsInEditMode = true;
      }
    }

    public static T FindFirstDescendantChildOf<T>(DependencyObject dpObj) where T : DependencyObject
    {
        if (dpObj == null) return null;

        for (var i = 0; i < VisualTreeHelper.GetChildrenCount(dpObj); i++)
        {
            var child = VisualTreeHelper.GetChild(dpObj, i);
            if (child is T) return (T)child;

            var obj = FindFirstChildOf<T>(child);

            if (obj != null) return obj;
        }

        return null;
    }

Update #2 (based on comments)

Add a property to the view that refers back to the the ViewModel assuming you keep a reference to the View Model in the DataContext:-

    .....  // Add this to the Window/Page

    public bool DetectingNewItems
    {
        get
        {
            var vm = DataContext as MyViewModel;
            if (vm != null)
                return vm.MyPropertyOnVM;
            return false;
        }
    }

    .....  
Chalfant answered 18/1, 2016 at 15:22 Comment(6)
Where is DetectingNewItems defined?Stopped
DetectingNewItems was a made up Boolean (a field in your view) you can use to mark the point when you want to start noticing new items. It is False when you want to populate the collection initially so the control will not be in edit mode if you are restoring the view from a datasource.Chalfant
Thanks a lot. I will test this later today and inform youStopped
How would you tell view to toggle DetectingNewItems without viewmodel not knowing what the view is?Stopped
I'd deduced you might be using MVVM but it is hard to see here how specifically you are doing it (loose MVVM, MVVM-light, full MVVM), I'll add an example of a property DetectingNewItems that points to a ViewModel property assuming you use the DataContext to hold the VMChalfant
That clears things out. Thanks a lot :) btw, wouldn't it be better to use dynamic instead of casting with as?Stopped
P
1

To get an element inside a template and change it's properties in code you need FrameworkTemplate.FindName Method (String, FrameworkElement) :

private childItem FindVisualChild<childItem>(DependencyObject obj)
where childItem : DependencyObject
{
    for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
    {
        DependencyObject child = VisualTreeHelper.GetChild(obj, i);
        if (child != null && child is childItem)
            return (childItem)child;
        else
        {
            childItem childOfChild = FindVisualChild<childItem>(child);
            if (childOfChild != null)
                return childOfChild;
        }
    }
    return null;
}

Then:

for (int i = 0; i < yourListBox.Items.Count; i++)
{
    ListBoxItem yourListBoxItem = (ListBoxItem)(yourListBox.ItemContainerGenerator.ContainerFromIndex(i));
    ContentPresenter contentPresenter = FindVisualChild<ContentPresenter>(yourListBoxItem);
    DataTemplate myDataTemplate = contentPresenter.ContentTemplate;
    EditableTextBlock editable = (EditableTextBlock) myDataTemplate.FindName("editableTextBlock", contentPresenter);
    //Do stuff with EditableTextBlock
    editable.IsInEditMode = true;
}
Pyrrhic answered 16/1, 2016 at 20:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.