Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get DataTemplate from data object in ListBox

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)

like image 371
wingerse Avatar asked Jan 04 '16 13:01

wingerse


People also ask

How do you find DataTemplate generated elements?

If you want to retrieve the TextBlock element generated by the DataTemplate of a certain ListBoxItem, you need to get the ListBoxItem, find the ContentPresenter within that ListBoxItem, and then call FindName on the DataTemplate that is set on that ContentPresenter.

What is DataTemplate?

A data template can contain elements that are each bound to a data property along with additional markup that describes layout, color and other appearance. DataTemplate is, basically, used to specify the appearance of data displayed by a control not the appearance of the control itself.

What is difference between a control template and DataTemplate in WPF?

A ControlTemplate will generally only contain TemplateBinding expressions, binding back to the properties on the control itself, while a DataTemplate will contain standard Binding expressions, binding to the properties of its DataContext (the business/domain object or view model).

Why to use data template?

DataTemplate objects are particularly useful when you are binding an ItemsControl such as a ListBox to an entire collection. Without specific instructions, a ListBox displays the string representation of the objects in a collection. In that case, you can use a DataTemplate to define the appearance of your data objects.


1 Answers

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

    .....  
like image 68
Rhys Avatar answered Sep 23 '22 08:09

Rhys