やる気駆動型エンジニアの備忘録

WPF(XAML+C#)の話題を中心に.NET/Android/CI やたまに趣味に関するブログです

OnRenderで再描画されない現象と対策

ハマったのでメモ

WPFアプリケーションであるコントロールをXamlではなくOnRenderメソッドをオーバーライドして自前で描画処理を実装していました。
ビルド、実行して問題なく動いていましたが、あることをするとOnRenderメソッドが呼ばれないことが発覚しました。

コントロールには次のような振る舞いを実装していました。

  • あるプロパティを変更することでコントロールの背景色を切り替える。
  • ControlTemplateは定義していない。OnRenderで描画処理を実装する。

そしてそのコントロールを画面に配置し、ボタンをクリックすると画面上のすべてのコントロールの状態を変更し、背景色を色替えするというものでした。
画面にはコントロールを数百個(以下の画面では300個)描画しています。
以下はその画面です。
f:id:iyemon018:20160418212103p:plain

画面右上のTestボタンをクリックすると全コントロールの状態を切り替えます。
以下はTestボタンをクリックした時の画面です。
f:id:iyemon018:20160418212146p:plain

なぜか一部のコントロールが色替えされていません。


コントロールと画面のコードは以下のとおりです。

  • コントロールクラス
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace CustomControls
{
    public class Symbol : Control
    {
        #region Dependency Properties

        /// <summary>
        /// TypeValue 依存関係プロパティを識別します。
        /// </summary>
        public static readonly DependencyProperty TypeValueProperty =
            DependencyProperty.Register(
                "TypeValue",
                typeof(short),
                typeof(Symbol),
                new FrameworkPropertyMetadata(
                    (short)0,
                    FrameworkPropertyMetadataOptions.AffectsRender,
                    TypeValuePropertyChanged));

        #endregion

        #region Ctor

        /// <summary>
        /// コンストラクタ
        /// </summary>
        static Symbol()
        {
            // -------------------------------------------------------------
            // コントロールテンプレート以外で必要な設定は
            // すべてここに記述します。
            // -------------------------------------------------------------
            DefaultStyleKeyProperty.OverrideMetadata(typeof(Symbol), new FrameworkPropertyMetadata(typeof(Symbol)));
            BackgroundProperty.OverrideMetadata(typeof(Symbol), new FrameworkPropertyMetadata(Brushes.White));
            BorderBrushProperty.OverrideMetadata(typeof(Symbol), new FrameworkPropertyMetadata(Brushes.Black));
        }

        #endregion

        #region Properties

        /// <summary>
        /// オブジェクトの状態種別を設定、取得します。
        /// </summary>
        [Category("Custom")]
        [Description("オブジェクトの状態種別を設定、取得します。")]
        public short TypeValue
        {
            get { return (short)GetValue(TypeValueProperty); }
            set { SetValue(TypeValueProperty, value); }
        }

        #endregion

        #region Methods

        /// <summary>
        /// レイアウトシステムからの描画処理に参加します。
        /// </summary>
        /// <param name="drawingContext">特定の要素に対する描画命令。 このコンテキストはレイアウト システムに提供されます。</param>
        protected override void OnRender(DrawingContext drawingContext)
        {
            base.OnRender(drawingContext);

            var background = TypeValue == 0 ? Brushes.Blue : Brushes.Red;
            var foreground = TypeValue == 0 ? Brushes.Aqua : Brushes.Orange;

            // 背景色と枠線色は同じ
            Background = background ?? Brushes.White;
            BorderBrush = background ?? Brushes.Black;
            Foreground = foreground ?? Brushes.Black;

            // ---------------------------------------------------------
            // 背景の角丸め長方形を描画
            // ---------------------------------------------------------
            drawingContext.DrawRoundedRectangle(
                Background,
                null,
                new Rect(0, 0, ActualWidth, ActualHeight),
                4.0d, 4.0d);
        }

        /// <summary>
        /// TypeValueプロパティの値が変更された際に呼ばれるイベントハンドラです。
        /// </summary>
        /// <param name="d">イベント送信元オブジェクト</param>
        /// <param name="e">イベント引数オブジェクト</param>
        private static void TypeValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var self = d as Symbol;
            if (self != null)
            {
                self.OnValueTypeChanged((short)e.NewValue);
            }
        }

        /// <summary>
        /// TypeValueの値が変更されました。
        /// </summary>
        /// <param name="newValue">変更後の値</param>
        private void OnValueTypeChanged(short newValue)
        {
            
        }

        #endregion
    }
}
<Window x:Class="RenderingTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow"
        Width="400"
        Height="400"
        Loaded="MainWindow_OnLoaded"
        WindowStartupLocation="CenterScreen">
    <Grid>
        <Canvas x:Name="LayoutRoot" />
        <Grid>
            <Button Width="75"
                    Height="28"
                    HorizontalAlignment="Right"
                    VerticalAlignment="Top"
                    Click="ButtonBase_OnClick"
                    Content="Test" />
        </Grid>
    </Grid>
</Window>
  • Viewクラス(コードビハインド)
using System;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using CustomControls;

namespace RenderingTest
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        /// <summary>
        /// 描画するオブジェクトの数
        /// </summary>
        const int Maximum = 300;

        public MainWindow()
        {
            InitializeComponent();
        }

        private void MainWindow_OnLoaded(object sender, RoutedEventArgs e)
        {
            var r = new Random();
            foreach (var i in Enumerable.Range(0, Maximum))
            {
                // サイズ固定
                var item = new Symbol
                {
                    Width = 12,
                    Height = 12,
                };

                // 描画座標はランダムに設定する。
                LayoutRoot.Children.Add(item);
                Canvas.SetLeft(item, r.Next((int)(LayoutRoot.ActualWidth - 12)));
                Canvas.SetTop(item, r.Next((int)(LayoutRoot.ActualHeight - 12)));
            }
        }

        /// <summary>
        /// Testボタンのクリックイベントハンドラ
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="RoutedEventArgs"/> instance containing the event data.</param>
        private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
        {
            // LayoutRoot(Canvas) の子要素(Symbol) に対して状態変更
            // これで全てのコントロールの背景色が切り替わる。
            foreach (var child in LayoutRoot.Children)
            {
                var item = child as Symbol;
                if (item != null)
                {
                    item.TypeValue = (short)(item.TypeValue == 0 ? 1 : 0);
                }
            }
        }
    }
}

このまま実行してもコンパイルエラーも発生しなければ、BindingExceptionも出力ウィンドウに表示されません。
さて、困ったものです。
同様の現象に遭遇した人を探すべく、"WPF OnRender doesn't work"で検索してみました。
すると以下のページがトップに。
stackoverflow.com

まさしく同じ現象!!
しかし、回答者も結局原因の究明には至っていない模様…

試行錯誤し、ひとつひとつの処理をチェックしていった結果
犯人はSymblo.csの以下のコードだということが判明しました。

            // 背景色と枠線色は同じ
            Background = background ?? Brushes.White;
            BorderBrush = background ?? Brushes.Black;
            Foreground = foreground ?? Brushes.Black;

OnRenderメソッドを以下のように書き換えてみます。

        /// <summary>
        /// レイアウトシステムからの描画処理に参加します。
        /// </summary>
        /// <param name="drawingContext">特定の要素に対する描画命令。 このコンテキストはレイアウト システムに提供されます。</param>
        protected override void OnRender(DrawingContext drawingContext)
        {
            base.OnRender(drawingContext);

            var background = TypeValue == 0 ? Brushes.Blue : Brushes.Red;
            var foreground = TypeValue == 0 ? Brushes.Aqua : Brushes.Orange;

            // ---------------------------------------------------------
            // 背景の角丸め長方形を描画
            // ---------------------------------------------------------
            drawingContext.DrawRoundedRectangle(
                background,
                null,
                new Rect(0, 0, ActualWidth, ActualHeight),
                4.0d, 4.0d);
        }

Background, Foreground, BorderBrush への代入を削除しました。
さて、実行してみます。

  • これが

f:id:iyemon018:20160418213531p:plain

  • こうなる。

f:id:iyemon018:20160418213541p:plain

無事に背景色の色替えができました。

結局何が原因だったのか?

すみません。具体的な原因についてはわかっておりません。m(_ _)m
ただ、Backgroundなど描画用のプロパティを代入することでOnRenderが実行される→OnRender内部で再度代入→また、OnRenderが呼ばれる…
と実行していった結果StackOverflowExceptionのような例外がWPFレンダリングエンジン内部で発行され、途中で描画が止まったのでは…?
ただ、そうだとするなら例外が発行されないのもおかしいし、一部だけが再描画に成功しているのもおかしい。
ということで原因をご存知の方がいらっしゃったら教えて下さい。

結論

OnRenderメソッドで描画関連プロパティへの代入は厳禁!!