有没有某个事件表明新的ContentTemplate已完全应用?

9

我有一个ContentControl,我想在某个事件中更改它的ContentTemplate。当ContentTemplate加载时,我想要添加一些值(将文本添加到TextBox中)。

但是,我发现在更改ContentTemplate属性后,新的ContentTemplate并不会立即应用(指加载新模板的所有控件)。

myContentControl.ContentTemplate = newContentTemplate;
// at this line controls of new template are not loaded!

我在那行代码后添加了以下代码进行测试:

var cp = GetVisualChild<ContentPresenter>(myContentControl);
var txt = myContentControl.ContentTemplate.FindName("Path_Cover", cp) as TextBox;
txt.Text = "test";

获取Visual元素的子元素

private T GetVisualChild<T>(DependencyObject parent) where T : Visual
    {
        T child = default(T);
        int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
        for (int i = 0; i < numVisuals; i++)
        {
            Visual v = (Visual)VisualTreeHelper.GetChild(parent, i);
            child = v as T;
            if (child == null)
            {
                child = GetVisualChild<T>(v);
            }
            if (child != null)
            {
                break;
            }
        }
        return child;
    }

我遇到了一个错误:

此操作仅适用于已应用此模板的元素。

是否有任何事件可以显示新的ContentTemplate已完全应用?

编辑1

@eran 我尝试了onApplyTemplate

public override void OnApplyTemplate()
{
   var cp = GetVisualChild<ContentPresenter>(Content_Option);
   var txt = Content_Option.ContentTemplate.FindName("Path_Cover", cp) as TextBox;
   txt.Text = "test";
}

但是出现了错误:

对象引用未设置到实例对象。

编辑2

这种“不规范”的方法完全没有问题:

myContentControl.ContentTemplate = newContentTemplate;

System.Windows.Threading.DispatcherTimer timer = new System.Windows.Threading.DispatcherTimer();
timer.Interval = TimeSpan.FromMilliseconds(0.000001);
timer.Tick += new EventHandler(delegate(object s, EventArgs a)
{
   timer.Stop();
   var cp = GetVisualChild<ContentPresenter>(Content_Option);
   var txt = Content_Option.ContentTemplate.FindName("Path_Cover", cp) as TextBox;
   txt.Text = "teSt";
});
timer.Start();

能否有人帮我用更加“干净”(专业)的方式来实现相同的结果:)

编辑3

我的场景是,我有一个TreeView(在左侧作为菜单)和一个Grid(在右侧作为ContentControl的显示器)。 TreeView有一些节点。每个节点都有自己的DataTemplate。每次单击TreeView节点时,会将DataTemplate设置为ContentControl,并从数据库中设置一个值(例如Path_Cover.Text)。 布局或多或少像Windows资源管理器。

好的,这是所有必要的代码:

XAML

    <UserControl.Resources>

      <DataTemplate x:Key="General">
        <StackPanel>
           <DockPanel>
               <TextBlock Text="Cover"/>
               <TextBox Name="Path_Cover"/>
           </DockPanel>
           <DockPanel>
               <TextBlock Text="Slide"/>
               <TextBox Name="Path_Slide"/>
           </DockPanel>
        </StackPanel>
      </DataTemplate>

      <DataTemplate x:Key="Appearance">
        <StackPanel>
           <DockPanel>
               <TextBlock Text="Cover"/>
               <TextBox Name="Path_Cover"/>
           </DockPanel>
           <DockPanel>
               <Button Content="Get Theme"/>
               <TextBox Name="Txt_Theme"/>
           </DockPanel>
        </StackPanel>
      </DataTemplate>

    <UserControl.REsources>

<Grid>
    <ContentControl Name="myContentControl"/>
</Grid>

后台代码

private void TreeMenu_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
   myContentControl.ContentTemplate =(DataTemplate)this.Resources[Tree_Menu.SelectedItem.ToString()];

   System.Windows.Threading.DispatcherTimer timer = new System.Windows.Threading.DispatcherTimer();
   timer.Interval = TimeSpan.FromMilliseconds(0.000001);
   timer.Tick += new EventHandler(delegate(object s, EventArgs a)
   {
      timer.Stop();
      switch (Tree_Menu.SelectedItem.ToString())
      {
         case "General": 
               var cp = GetVisualChild<ContentPresenter>(Content_Option);
               var txt = Content_Option.ContentTemplate.FindName("Path_Cover", cp) as TextBox;
               txt.Text = "test";

               txt = Content_Option.ContentTemplate.FindName("Path_Slide", cp) as TextBox;
               txt.Text = "test";
               break;

        case "Appearance": 
               var cp = GetVisualChild<ContentPresenter>(Content_Option);
               var txt = Content_Option.ContentTemplate.FindName("Txt_Theme", cp) as TextBox;
               txt.Text = "test";
               break;
      }
   });
   timer.Start();
}

我只需要将定时器事件处理程序中的代码“移动”到完全应用DataTemplate / ContentTemplate后触发的某个新事件中。

OnLoad或OnLoadData在OnApplyTemplate之后被调用。请查看MSDN。 - eran otzap
@eran Onload事件只会触发一次。我想要一个事件,每次我更改ContentTemplate时都会触发。 - Reyn
3个回答

5

我知道这是一个比较老的问题,但我一直在寻找答案,并且已经想出了一个解决方案,所以觉得这里是分享它的好地方。

我仅仅是创建了自己的ContentPresenter类,它继承于标准的ContentPresenter:

public class ContentPresenter : System.Windows.Controls.ContentPresenter {

    #region RE: ContentChanged
    public static RoutedEvent ContentChangedEvent = EventManager.RegisterRoutedEvent("ContentChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ContentPresenter));
    public event RoutedEventHandler ContentChanged {
        add { AddHandler(ContentChangedEvent, value); }
        remove { RemoveHandler(ContentChangedEvent, value); }
    }
    public static void AddContentChangedHandler(UIElement el, RoutedEventHandler handler) {
        el.AddHandler(ContentChangedEvent, handler);
    }
    public static void RemoveContentChangedHandler(UIElement el, RoutedEventHandler handler) {
        el.RemoveHandler(ContentChangedEvent, handler);
    }
    #endregion

    protected override void OnVisualChildrenChanged(System.Windows.DependencyObject visualAdded, System.Windows.DependencyObject visualRemoved) {
        base.OnVisualChildrenChanged(visualAdded, visualRemoved);
        RaiseEvent(new RoutedEventArgs(ContentChangedEvent, this));
    }
}

我希望这可以帮助那些正在寻找ContentPresenter设计中的明显疏漏的简单解决方案的人。

2
我不认为在WPF框架中有这样的事件。但是,您可以确保在新分配的内容模板应用后运行代码。
实现这一点的方法(以及您“不太好”的解决方案的“正确”版本)是利用与您的ContentControl相关联的 Dispatcher 。此代码将完成您想要实现的功能:
myContentControl.ContentTemplate = newContentTemplate;
myContentControl.Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() =>
{
    var cp = GetVisualChild<ContentPresenter>(myContentControl);
    var txt = myContentControl.ContentTemplate.FindName("Path_Cover", cp) as TextBox;
    txt.Text = "test";
}));

注意,该代码将执行的优先级设置为DispatcherPriority.Loaded,因此它将与您放置在FrameworkElement.Loaded事件处理程序中的代码具有相同的优先级执行。

0

一般来说,我不知道任何这种类型的事件。但是对于您的情况,典型的 WPF 方法是:

<ContentControl Name=myContentControl>
<ContentControl.ContentTemplate>
    <DataTemplate>
        <StackPanel>
            ...other controls here
            <TextBox Text={Binding Mode=TwoWay}/>
            ... more controls here
        </StackPanel>
    </DataTemplate>
</ContentControl.ContentTemplate>

代码后台:

myContentControl.Content = "Test";

或者您可以将Content绑定到(ViewModel的属性中),并在其中放置代码。

如果您想要访问内容模板内部的控件,只需为其命名,并从应用了内容模板的控件执行FindName操作。不需要使用VisualChild来搜索ContentPresenter。

我有一种感觉,您将控件模板和数据模板(contenttemplate,itemtemplate)混淆了。

  1. OnApplyTemplate是指应用ControlTemplate的时刻,而不是ContentTemplate或任何其他数据模板。
  2. ContentPresenter是ControlTemplate的典型成分,而不是ContentTemplate。

非常抱歉,我对wpf很新。我需要先学习很多东西(包括mvvm的概念)。你能否只是“跟随”我上面的代码,并根据我的流程给我直接的答案(代码)呢?我只是需要将定时器中的代码“移动”到一些新的事件中。 - Reyn
有时候我也会对DataTemplate、ContentTemplate、ItemTemplate和ContentControl感到困惑,呵呵。 - Reyn
我有一种感觉,你想要实现一些简单的东西,但却考虑了复杂的解决方案。实际上,在内容呈现器中不需要遍历可视树,也就没有必要使用计时器。保持简单:使用两个内容控件,并在所选内容上切换可见性,这样就可以使用在数据模板中定义的内容。从那里开始,尝试制作一个更好的解决方案。 - Grafix
是的,可以通过简单地更改所选网格的可见性来实现,而无需使用所有DataTemplates和ContentControls。但是,就像我说的那样,我仍在学习,所以有时我想用不寻常的方式实现某些东西:P。这就是其中之一。 - Reyn

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