0

I can successfully draw part of a custom control in the control's OnApplyTemplate method, however, this method is called only once and does not appear to be called when I called ApplyTemplate when the control's Num property changes.

Here is the custom control's code:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;

namespace Cc1
{
    public class CirclesControl : Control
    {
        static CirclesControl()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(CirclesControl), new FrameworkPropertyMetadata(typeof(CirclesControl)));
        }

        public int Num
        {
            get { return (int)GetValue(NumProperty); }
            set { SetValue(NumProperty, value); }
        }

        public static readonly DependencyProperty NumProperty =
            DependencyProperty.Register("Num", typeof(int), typeof(CirclesControl), new PropertyMetadata(2, new PropertyChangedCallback(OnNumChanged)));

        private static void OnNumChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (d is CirclesControl control)
            {
                System.Diagnostics.Debug.WriteLine($"Num changed - value is now {control.Num}");
                control.ApplyTemplate();
            }
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            var canvasPart = Template.FindName("Part_Canvas", this);
            if (canvasPart is Canvas canvas)
            {
                for (int i = 0; i < Num; i++)
                {
                    var circle = new Ellipse
                    {
                        Height = 50,
                        Width = 50,
                        Fill = Brushes.Red,
                        Stroke = Brushes.Green
                    };
                    canvas.Children.Add(circle);
                    Canvas.SetLeft(circle, i * 55);
                    Canvas.SetTop(circle, 50);
                }
            }
        }
    }
}

I know that OnNumChanged is called because System.Diagnostics.Debug.WriteLine writes a message to the output window. OnNumChanged also calls ApplyTemplate but this does not cause OnApplyTemplate to be called.

Here is the main window's xaml:

<Window x:Class="Cc1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Cc1"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="1024">
<Window.DataContext>
    <local:ViewModel/>
</Window.DataContext>
    <StackPanel>
        <TextBlock Text="{Binding NumberOfCircles}" FontSize="36"/>
        <local:CirclesControl Num="{Binding NumberOfCircles}"/>
    </StackPanel>
</Window>

The generic.xaml for the control:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:Cc1">


    <Style TargetType="{x:Type local:CirclesControl}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:CirclesControl}">
                    <Border Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}">
                        <Canvas x:Name="Part_Canvas"/>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

And the viewmodel:

using System;
using System.Windows;
using System.Windows.Threading;

namespace Cc1
{
    public class ViewModel : DependencyObject
    {
        public ViewModel()
        {
            var timer = new DispatcherTimer
            {
                Interval = TimeSpan.FromMilliseconds(500)
            };
            timer.Tick += (s, e) =>
            {
                NumberOfCircles++;
                if (NumberOfCircles == 10)
                {
                    NumberOfCircles = 2;
                }
            };
            timer.Start();
        }
        public int NumberOfCircles
        {
            get { return (int)GetValue(NumberOfCirclesProperty); }
            set { SetValue(NumberOfCirclesProperty, value); }
        }

        public static readonly DependencyProperty NumberOfCirclesProperty =
            DependencyProperty.Register("NumberOfCircles", typeof(int), typeof(ViewModel), new PropertyMetadata(4));
    }
}

The number of circles (in the view model) changes all the time (this is what the dispatcher timer does). The TextBlock on the main windows accurately reflects the number of circles however the CirclesControl custom control on the main window does not update.

Grateful for any hints as to what is missing.

Adrian S
  • 514
  • 7
  • 16
  • 1
    You can move that part into a method, add `canvas.Children.Clear()` in beginning and call it from [propdp changed event](https://stackoverflow.com/a/8830205/1997232)... but you know how flexible WPF is? What if somewhere you want to use this control with rectangles instead of ellipses or change their color/size? Moving that hard-coded part into xaml and using bindings/data templates/triggers might be a better idea in a long go. – Sinatr Jan 23 '19 at 13:24
  • @sinatr, thanks. Creating a method which adds circles to the canvas solves the problem. I wouldn't mind knowing why `OnApplyTemplate` isn't called when I call `ApplyTemplate` though. On your second point, moving the code into XAML is not an option - the example I gave is not a real-world example, but just a small project I created to illustrate the problem. The real-world code is a much more complex control which cannot be drawn using XAML. – Adrian S Jan 23 '19 at 14:50
  • *"why OnApplyTemplate isn't called when I call ApplyTemplate"* - the assumption what calling `XXX` will call `OnXXX` is wrong. `XXX` will do something and sometimes (if conditions are appropriate, certain event should be rised, etc.) call `virtual OnXXX`. Keyword - *sometimes* (you can see [source](https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/FrameworkElement.cs,351)). – Sinatr Jan 23 '19 at 15:03

0 Answers0