x:Name探訪(その3) ContentControl とか ItemsControl などなどにくっついていない要素の取得

Silverlight 2 の時は、カスタムコントロールの作りによっては FindName しても取得できない場合がありました。ちょっと完全にこれだという部分までは突き止めていないのですが、動きを見ているに ContentControl とか ItemsControl とか Panel の子供とか以外でカスタムコントロールを作って、内部でテンプレートの中にある要素にくっつけた場合にダメになっているようでした。なので ViewBox に配置した要素は FindName しても取得できませんでした。そのため VS が自動生成してくれるコントロールの変数の中身も null でした。


で、これが Silverlight 3 になってどうなったのかなと思って ViewBox で試してみるとちゃんと取れたのです。ViewBox の作りが変わったのかなと思いつつ、ちょっと調べるために自分でカスタムコントロール作ってみてみました。

<Style TargetType="my:SilverlightCustomControl1">
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="my:SilverlightCustomControl1">
        <Border x:Name="ContentHolder" Background="Red" Padding="10" />
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>
[TemplatePart(Name = "ContentHolder", Type = typeof(Border))]
[ContentProperty("Child")]
public class SilverlightCustomControl1 : Control {

  private Border ContentHolder = null;

  public SilverlightCustomControl1() {
    this.DefaultStyleKey = typeof(SilverlightCustomControl1);
  }

  public override void OnApplyTemplate() {
    base.OnApplyTemplate();
    
    this.ContentHolder = this.GetTemplateChild("ContentHolder") as Border;
    PrepareContent();
  }

  public UIElement Child {
    get { return (UIElement)GetValue(ChildProperty); }
    set { SetValue(ChildProperty, value); }
  }

  public static readonly DependencyProperty ChildProperty =
    DependencyProperty.Register("Child", typeof(UIElement), 
    typeof(SilverlightCustomControl1), 
    new PropertyMetadata(OnChildChanged));

  private static void OnChildChanged(object sender, 
    DependencyPropertyChangedEventArgs e) {
    
    ((SilverlightCustomControl1)sender).OnChildChanged(
      (UIElement)e.NewValue, (UIElement)e.OldValue);
  }

  private void OnChildChanged(UIElement newValue, UIElement oldValue) {
    PrepareContent();
  }

  private void PrepareContent() {
    if(this.ContentHolder == null){
      return;
    }
    this.ContentHolder.Child = this.Child;
  }
}

上みたいなコントロールを作ってみました。PrepareContent の中で Child の要素を ContentHolder って名前の Border にくっつけています。


これを、MainPage で xaml に設定します。Child には Rectangle を設定して ContentElement って名前を付けてみました。

<UserControl x:Class="XNameResearch1.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:controls="clr-namespace:XNameResearch1.Controls"
    Width="400" Height="300">
  <StackPanel x:Name="LayoutRoot" Background="Blue">
    <Button Click="Button_Click" Content="Click Me!" />
    <controls:SilverlightCustomControl1>
      <Rectangle Width="100" Height="100" Fill="Green" x:Name="ContentElement" />
    </controls:SilverlightCustomControl1>
  </StackPanel>
</UserControl>

前回の投稿と似たような感じですね。無事に表示されています。で、やっぱりボタンの Click イベントで FindName 出来るかどうかを見てみます。

private void Button_Click(object sender, RoutedEventArgs e) {
  object element = this.FindName("ContentElement");
  MessageBox.Show(element == null ? "Not found..." : "Found !");
}

おぉ、無事に動きました。
ちなみに、VS が自動生成してくれる ContentElement 変数も null にはならずちゃんとインスタンスが入っています。


ちなみに、カスタムコントロールの中の PrepareContent しているところで Border に設定しないとどうなるかを試してみました。

private void PrepareContent() {
  if(this.ContentHolder == null){
    return;
  }
  //this.ContentHolder.Child = this.Child;
}

Child を設定していないので、当然のことながら Rectangle 要素は画面に表示されません。ここで、同じようにボタンをクリックしてみると、、、、

おぉ、これもいけました。ということは FindName で引っかかる対象は VisualTree の中に入っている必要がないのかって推測できます。もっと詳細に確かめないと確証は持てませんが。