从ListBox中的数据对象获取DataTemplate

6
我是一名有用的助手,可以为您翻译文本。

我有一个 ListBox ,其 ItemTemplate 如下所示:

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

Column是一个简单的类,长这样:

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。当为true时,将显示TextBox

问题:
ListBox的ItemsSouce是一个ObservableCollection<Column>。我有一个按钮,可以向集合中添加新的Column。但我的问题是,我希望通过该按钮IsInEditMode设置为true,以便于新添加的EditableTextBlock。但我只能在ViewModel中访问Column。如何访问ItemsSource集合中指定ColumnEditableTextBlock

我能想到的唯一解决方案是从Column派生一个类,并添加一个属性(例如:名称:IsInEditMode)(或者使用包装器类。这里有一个类似的答案,建议使用包装器类),然后在DataTemplate中绑定该属性,如下所示:
<DataTemplate DataType="local:DerivedColumn">
    <utils:EditableTextBlock x:Name="editableTextBlock" Text="{Binding Name, Mode=TwoWay}"
                             IsInEditMode="{Binding IsInEditMode}"/>
</DataTemplate>

但我不想这样做。我希望有一种在XAML中完成此操作的方法,而无需派生类并添加不必要的代码。(同时遵守MVVM规则)


为什么不在Column类中添加属性,将其绑定到IsInEditMode DP,并创建一个带有可选参数的构造函数来设置模式?您只需要创建一个Column并在按钮的Command中传递true给构造函数即可。 - nkoniishvt
我知道。那是我使用派生类做的。但是我还将在其他地方使用这个Column类。而且在那些地方,IsInEditMode属性将没有用处。 有没有仅使用XAML的解决方案? - wingerse
我刚刚做了一个包装类而不是派生它。 - wingerse
2个回答

2
如果您有添加新依赖属性到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 都不处于编辑模式,然后在开始添加新项目时将 StartupInEditMode 模式切换为 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 ItemsSource时,将ANewViewProperty的值设置为False。单击按钮以向列表添加新项时,将ANewViewProperty的值设置为True,表示现在将创建处于编辑模式的控件。
更新:仅限C#和视图的替代方法
代码-仅视图的替代方法(类似于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 中保留了对ViewModel的引用:

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

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

    .....  

检测新项目的定义在哪里? - wingerse
“DetectingNewItems”是您视图中的一个布尔字段,用于标记您想要开始注意新项目的时间点。如果您正在从数据源恢复视图,则在初始填充集合时它为“False”,因此如果您正在从数据源恢复视图,则控件不会处于编辑模式。 - Rhys
非常感谢。我会在今天稍后测试并通知您。 - wingerse
你如何告诉视图在不让视图模型知道视图的情况下切换DetectingNewItems? - wingerse
我猜你可能在使用MVVM,但是很难看出你具体是如何使用的(宽松MVVM、MVVM-Light、完全MVVM)。我会添加一个示例属性DetectingNewItems,假设你使用DataContext来保存ViewModel。 - Rhys
这样就清楚了。非常感谢 :) 顺便问一下,使用dynamic而不是用as进行强制转换会更好吗? - wingerse

1
为了在代码中获取模板内的元素并更改其属性,您需要使用 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;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接