Volunteer .NET Evangelist

A well oiled machine can’t run efficiently, if you grease it with water.
  首页 :: 联系 :: 订阅 订阅 :: 管理

DataTemplate Is A Bit Naughtier To Play With

Posted on 2006-09-23 23:05 Sheva 阅读(...) 评论(...) 编辑 收藏
    Some one pointed out on MSDN WPF forum post that FindName breaks on DataTemplate, when we want to reference the visual elements inside the data template, we cannot use FindName, then what we can use? unfortunately, the only solution currently I can think of is to traverse through the visual tree of the templated element, with some helper classes and methods, we can make this process a bit easier. First I create a VisualTreeWalker class which houses all the logic of walking through the tree.

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
 
namespace Sheva.Windows.Components
{
    public delegate void VisualVisitedEventHandler(Object sender, VisualVisitedEventArgs e);
 
    /// <summary>
    /// This class represents a wrapper around the arguments passed to the VisualVisited event handler.
    /// </summary>
    /// <remarks>
    /// This class is mutated from John Grossman's VisualTreeWalker implementation.
    /// </remarks>
    public class VisualVisitedEventArgs : EventArgs
    {
        private Visual visitedVisual;
        private Int32 currentDepth;
 
        public VisualVisitedEventArgs(Visual visitedVisual, Int32 currentDepth)
        {
            this.visitedVisual = visitedVisual;
            this.currentDepth = currentDepth;
        }
 
        /// <summary>
        /// Get the visual currently visited by the VisualTreeWalker.
        /// </summary>
        public Visual VisitedVisual
        {
            get { return visitedVisual; }
        }
 
        /// <summary>
        /// Get the depth of visual tree that the VisualTreeWalker is currently in.
        /// </summary>
        public Int32 CurrentDepth
        {
            get { return currentDepth; }
        }
    }
 
    /// <summary>
    /// Represents a class which can walk through the visual tree.
    /// </summary>
    public class VisualTreeWalker
    {
        private Int32 visualCount;
 
        /// <summary>
        /// Presents the VisualVisited event.
        /// </summary>
        public event VisualVisitedEventHandler VisualVisited;
 
        /// <summary>
        /// Begin to walk through the visual tree starting from the reference visual.
        /// </summary>
        /// <param name="reference">The visual whose visual tree will be walked through.</param>
        /// <returns>The number of visuals visited by the VisualTreeWalker.</returns>
        public Int32 Walk(Visual reference)
        {
            this.visualCount = 0;
            this.TraverseVisuals(reference, 1);
            return this.visualCount;
        }
 
        private void TraverseVisuals(Visual visual, Int32 currentDepth)
        {
            this.visualCount++;
            this.OnVisualVisited(new VisualVisitedEventArgs(visual, currentDepth));
 
            for (Int32 i = 0; i < VisualTreeHelper.GetChildrenCount(visual); i++)
            {
                Visual child = VisualTreeHelper.GetChild(visual, i) as Visual;
                this.TraverseVisuals(child, currentDepth++);
            }
        }
 
        /// <summary>
        /// Raise the VisualVisited event.
        /// </summary>
        /// <param name="e">Arguments passed to the VisualVisited event handler.</param>
        protected void OnVisualVisited(VisualVisitedEventArgs e)
        {
            if (this.VisualVisited != null)
            {
                this.VisualVisited(this, e);
            }
        }
    }
}



    The above code is mutated from John Grossman's code, I just add a VisualVisited event to the class to make it a bit straightforward to work with.
    Next, I define a VisualTreeUtility class, and defines two helper methods which will leverage upon the code I pasted above:

using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Controls;
 
namespace Sheva.Windows.Components
{
    public class VisualTreeUtility
    {
        
        /// <summary>
        /// Find the child element in the visual tree of reference visual.
        /// </summary>
        /// <typeparam name="T">The type of element you want to examine.</typeparam>
        /// <param name="reference">The visual whose visual tree is traversed.</param>
        /// <returns>Return the element being first found in the visual tree</returns>
        public static T FindChildVisual<T>(Visual reference) where T : Visual
        {
            T targetVisual = null;
            VisualTreeWalker walker = new VisualTreeWalker();
            walker.VisualVisited += delegate(object sender, VisualVisitedEventArgs e)
            {
                if (e.VisitedVisual is T)
                {
                    targetVisual = e.VisitedVisual as T;
                }
            };
 
            walker.Walk(reference);
            return targetVisual;
        }
 
        /// <summary>
        /// Find the child elements in the visual tree of reference visual.
        /// </summary>
        /// <typeparam name="T">The type of element you want to examine.</typeparam>
        /// <param name="reference">The visual whose visual tree is traversed.</param>
        /// <returns>Return the elements being found in the visual tree</returns>
        public static T[] FindChildVisuals<T>(Visual reference) where T : Visual
        {
            List<T> visuals = new List<T>();
            VisualTreeWalker walker = new VisualTreeWalker();
            walker.VisualVisited += delegate(object sender, VisualVisitedEventArgs e)
            {
                if (e.VisitedVisual is T)
                {
                    visuals.Add((T)e.VisitedVisual);
                }
            };
 
            walker.Walk(reference);
            return visuals.ToArray();
        }
    }
}

    Note that I use the generic to void casting Visual type back to the type say ListBox we actually need,and the usage of this helper class is even more straightforward, for instance if I have following xaml definition:

<Window x:Class="VisualTreeWalkerDemo.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Loaded="WindowLoaded"
    >
  <Window.Resources>
    <XmlDataProvider x:Key="xmlData">
      <x:XData>
        <ListBoxItems xmlns="">
          <ListBoxItem Name="Item One"/>
          <ListBoxItem Name="Item Two"/>
          <ListBoxItem Name="Item Three"/>
          <ListBoxItem Name="Item Four"/>
        </ListBoxItems>
      </x:XData>
    </XmlDataProvider>
    <DataTemplate x:Key="dataTemplate">
      <TextBlock Text="{Binding XPath=@Name}"/>
    </DataTemplate>
  </Window.Resources>
    <Grid>
      <ListBox x:Name="myListBox" Width="300" Height="300" 
               ItemTemplate="{StaticResource dataTemplate}" 
               ItemsSource="{Binding Source={StaticResource xmlData}, XPath=ListBoxItems/ListBoxItem}"/>
    </Grid>
</Window>


    Then I can easily get the TextBlocks inside the ListBox's ItemTemplate:

        private void WindowLoaded(Object sender, RoutedEventArgs e)
        {
            List<TextBlock> items = VisualTreeUtility.FindChildVisuals<TextBlock>(myListBox);
            if (items.Count > 0)
            {
                foreach (TextBlock item in items)
                {
                    item.PreviewMouseLeftButtonUp += item_PreviewMouseLeftButtonUp;
                }       
            }
        }
 
        private void item_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            MessageBox.Show("I got clicked");
        }