lecloneur lecloneur - 4 months ago 56
C# Question

FindParent in WPF CustomControl return null

I'm trying to make a circular slider (a customcontrol) to select HUE of a color. I'm facing this problem when running my demo app. FindParent, "_templateCanvas" in this case, is always null but can't find out why.

Here is part of the HueWheel class :

public class HueWheel : Slider
{
static HueWheel()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(HueWheel), new FrameworkPropertyMetadata(typeof(HueWheel)));
}
private bool _isPressed = false;
private Canvas _templateCanvas = null;

protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
_isPressed = true;
}

protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
_isPressed = false;
}

protected override void OnMouseMove(MouseEventArgs e)
{
if (_isPressed)
{
if (_templateCanvas == null)
{
_templateCanvas = MyHelper.FindParent<Canvas>(e.Source as Ellipse);
if (_templateCanvas == null) return;
}

const double RADIUS = 150;
Point newPos = e.GetPosition(_templateCanvas);
double angle = MyHelper.GetAngleR(newPos, RADIUS);
//huewheel.Value = (huewheel.Maximum - huewheel.Minimum) * angle / (2 * Math.PI);
}
}
}


This is the class to find the parent :

public static class MyHelper
{
public static T FindParent<T>(FrameworkElement current) where T : FrameworkElement
{
do
{
current = VisualTreeHelper.GetParent(current) as FrameworkElement;
if (current is T)
{
return (T)current;
}
}
while (current != null);
return null;
}


Here is what I have in Generic.xaml

<Style TargetType="{x:Type local:HueWheel}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:HueWheel}">
<Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}">
<Slider Name="huewheel">
<Slider.Template>
<ControlTemplate>
<Viewbox>
<Canvas Width="300" Height="300">
<Image Stretch="Fill" Source="Assets/HueCircle.PNG" Focusable="False" Height="300" Width="300" RenderTransformOrigin="0.5,0.5">
<Image.RenderTransform>
<TransformGroup>
<ScaleTransform/>
<SkewTransform/>
<RotateTransform Angle="270"/>
<TranslateTransform/>
</TransformGroup>
</Image.RenderTransform>
</Image>
<Ellipse Fill="Transparent" Width="300" Height="300" Canvas.Left="0" Canvas.Top="0"/>
<Canvas>
<Line Stroke="Transparent" StrokeThickness="5" X1="150" Y1="150" X2="150" Y2="0"/>
<Ellipse Fill="Black" Width="20" Height="20" Canvas.Left="140" Canvas.Top="30"/>
<Canvas.RenderTransform>
<RotateTransform CenterX="150" CenterY="150">
<RotateTransform.Angle>
<MultiBinding Converter="{StaticResource ValueAngleConverter}">
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Value"/>
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Minimum"/>
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Maximum"/>
</MultiBinding>
</RotateTransform.Angle>
</RotateTransform>
</Canvas.RenderTransform>
</Canvas>
</Canvas>
</Viewbox>
</ControlTemplate>
</Slider.Template>
</Slider>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>


The canvas exist in the xaml, the ellipse I'm mouving the mouse on is contained in this canvas, so I should be able to get the ellipse parent (the canvas).

Any idea ?

Thank you

Answer

Or, "What's the Matter with Canvas?"

I tried testing your code, and I wasn't surprised to find that e.Source was of type HueWheel, which inherits from Slider and not from Ellipse. For that reason, this expression always returns null:

e.Source as Ellipse

...therefore, a) e.Source is not the object you thought it was, and b) this next statement always sets _templateCanvas to null:

_templateCanvas = MyHelper.FindParent<Canvas>(e.Source as Ellipse);

FindParent returns null immediately because you always pass it a null for the current parameter. Neither canvas is a parent of the HueWheel anyhow (which you knew -- what you didn't know was that e.Source was the HueWheel -- but the first thing you'll know to do next time is slap in a breakpoint and hover the mouse over e.Source to see what you've got).

So now let's fix it. What follows is How You Do It In WPF, pretty much, except for whatever details the real smart guys hassle me about in comments.

I don't know which Canvas you're trying to grab here, so I'll grab you both of them. First, give them x:Name properties in the template; I'll call them PART_FirstCanvas and PART_SecondCanvas because I didn't figure out the semantics. I've also rewritten your template to eliminate the inner slider; I don't understand why you put a retemplated slider in a template for a slider. The trouble there is that I was unable to call the inner slider's GetTemplateChild() method, because it's protected. My other option was to write a minimal subclass of Slider which merely exposes GetTemplateChild() publically, and use that for the inner slider. If there's a good reason to have two sliders which wasn't obvious to me (maybe one's an X coordinate and the other's Y?), we can update this to do it that way.

<ControlTemplate 
    TargetType="{x:Type test:HueWheel}"
    >
    <Border 
        Background="{TemplateBinding Background}" 
        BorderBrush="{TemplateBinding BorderBrush}" 
        BorderThickness="{TemplateBinding BorderThickness}"
        >
        <Viewbox>
            <Canvas 
                Width="300" 
                Height="300"
                x:Name="PART_FirstCanvas"
                >
                <Image 
                    Stretch="Fill" 
                    Source="Assets/HueCircle.PNG"
                    Focusable="False" 
                    Height="300" 
                    Width="300" 
                    RenderTransformOrigin="0.5,0.5"
                    >
                    <Image.RenderTransform>
                        <TransformGroup>
                            <ScaleTransform/>
                            <SkewTransform/>
                            <RotateTransform Angle="270"/>
                            <TranslateTransform/>
                        </TransformGroup>
                    </Image.RenderTransform>
                </Image>
                <Ellipse 
                    Fill="Transparent" 
                    Width="300" 
                    Height="300" 
                    Canvas.Left="0" 
                    Canvas.Top="0"/>
                <Canvas
                    x:Name="PART_SecondCanvas">
                    <Line Stroke="Transparent" StrokeThickness="5" X1="150" Y1="150" X2="150" Y2="0"/>
                    <Ellipse Fill="Black" Width="20" Height="20" Canvas.Left="140" Canvas.Top="30"/>
                    <Canvas.RenderTransform>
                        <RotateTransform CenterX="150" CenterY="150">
                            <RotateTransform.Angle>
                                <MultiBinding Converter="{StaticResource ValueAngleConverter}">
                                    <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Value"/>
                                    <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Minimum"/>
                                    <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Maximum"/>
                                </MultiBinding>
                            </RotateTransform.Angle>
                        </RotateTransform>
                    </Canvas.RenderTransform>
                </Canvas>
            </Canvas>
        </Viewbox>
    </Border>
</ControlTemplate>

Next, we'll add private members for those two named Canvases, and grab them in an override of OnApplyTemplate(). Then we'll use whichever we want in that mousemove event:

public class HueWheel : Slider
{
    static HueWheel()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(HueWheel), new FrameworkPropertyMetadata(typeof(HueWheel)));
    }

    private bool _isPressed = false;
    private Canvas _PART_FirstCanvas;
    private Canvas _PART_SecondCanvas;

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();

        //  Use () cast rather than the "as" operator because if the actual runtime 
        //  type can't be cast to the desired type, that'll throw an exception 
        //  rather than silently returning null. If you had cast (Ellipse)e.Source, 
        //  that would have blown up on you because you can't cast HueWheel to Ellipse. 
        //  That would have instantly shown you what was wrong. 
        //  But you won't get an exception here if GetTemplateChild() just returns null. 
        _PART_FirstCanvas = (Canvas)GetTemplateChild("PART_FirstCanvas");
        _PART_SecondCanvas = (Canvas)GetTemplateChild("PART_SecondCanvas");
    }

    protected override void OnMouseMove(MouseEventArgs e)
    {
        if (_isPressed)
        {
            const double RADIUS = 150;

            //  Or _PART_SecondCanvas; whichever. 
            Point newPos = e.GetPosition(_PART_FirstCanvas);

            double angle = MyHelper.GetAngleR(newPos, RADIUS);
            //huewheel.Value = (huewheel.Maximum - huewheel.Minimum) * angle / (2 * Math.PI);
        }
    }

    protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
    {
        _isPressed = true;
    }

    protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
    {
        _isPressed = false;
    }
}