从 ListBox 中的数据对象获取 DataTemplate

Get DataTemplate from data object in ListBox

我有一个 ListBox,它的 ItemTemplate 看起来像这样:

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

Column 是一个简单的 class,看起来像这样:

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

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

EditableTextBlock 是一个 UserControl,双击时变成 TextBox,失去焦点时又变回 TextBlock。它还有一个名为 IsInEditMode 的 属性,默认情况下为 false。如果为真,则显示 TextBox

问题:
ListBox 的 ItemsSouce 是一个 ObservableCollection<Column>。我有一个按钮,可以将新的 Column 添加到集合中。但我的问题是,我希望 IsInEditMode 通过 那个按钮 为新添加的 EditableTextBlock 变为真。但是我只能在 ViewModel 中访问 Column。如何访问 ItemsSource 集合中指定 ColumnEditableTextBlock

我能想出的唯一解决方案是从 Column 导出 class 并为此添加一个 属性(例如:名称:IsInEditMode)(或者可能是包装器 class。Here 的类似答案建议使用包装器 class) 并绑定到 DataTemplate 中的那个 属性,如下所示:

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

但我不想要这个。我想要一些方法在 XAML 中执行此操作,而无需派生 classes 并添加不必要的代码。 (并且还遵守 MVVM 规则)

要获取模板中的元素并在代码中更改它的属性,您需要 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;
}

然后:

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

如果您可以向 EditableTextBlock 用户控件添加一个新的依赖项 属性,您可以考虑添加一个名称为 StartupInEditMode 的依赖项,默认为 false保持现有行为。

UserControlLoaded 处理程序然后可以确定 StartupInEditMode 的状态,以决定如何初始设置 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;
    }

对于已经在可视化树中的控件,StartupInEdit 的变化值无关紧要,因为它只在创建时评估一次。这意味着您可以填充 ListBox 的集合,其中每个 EditableTextBlock 都不处于编辑模式,然后在开始添加新项目时将 StartupInEditmMode 模式切换为 True。然后每个新的 EditableTextBlock 控件在编辑模式下启动。

您可以通过指定 DataTemplate 来完成此行为切换,其中此新 属性 的 Binding 指向视图的变量而不是集合项。

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

您需要添加一个 属性 到父级 Window(或 Page 或任何用作视图容器的对象),在此示例中称为 ANewViewProperty .如果您将绑定更改为 {Binding DataContext.ANewViewProperty, RelativeSource={RelativeSource AncestorType={x:Type Window}}}.

,则此值可能是您的视图模型的一部分

这个新的 属性 (ANewViewProperty) 甚至不需要实现 INotifyPropertyChanged 因为绑定将在创建新的 EditableTextBlock 控件时获得初始值如果以后更改该值,则无论如何都没有影响。

您可以在最初加载 ListBox ItemSource 时将 ANewViewProperty 的值设置为 False。当您按下按钮将新项目添加到列表时,将 ANewViewProperty 的值设置为 True,这意味着现在将在编辑模式下启动创建的控件。

更新:仅 C#,View-only 替代方案

code-only、view-only 替代方案(类似于 user2946329 的回答)是挂钩 ListBox.ItemContainerGenerator.ItemsChanged 处理程序,该处理程序将在添加新项目时触发。一旦触发,您现在正在对新项目进行操作(通过布尔值 DetectingNewItems),它会为新添加的项目找到适当的 ListBoxItem 视觉容器的第一个后代 EditableTextBlock 控件。获得控件引用后,更改 IsInEditMode 属性.

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

更新 #2(基于评论)

在视图中添加一个 属性 来引用 ViewModel,假设您在 DataContext 中保留对视图模型的引用:-

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

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

    .....