WPF DataGrid 行の色が更新されない・点滅が同期しない原因と解決策(INotifyPropertyChanged/Storyboard)

WPF の DataGrid で「行データの COLOR 値に応じて赤/緑/点滅に切り替える」実装は定番ですが、行を追加すると点滅開始がズレる、ACK で COLOR=3→1 にしても点滅が止まらない、といったトラブルに遭遇しがちです。原因は “通知の不足” と “アニメーションの優先順位” にあります。ここでは切り分けポイントと、すぐ効く回避策から正攻法までを具体例付きで整理します。

目次

前提:今回の DataGrid 行色制御の仕様を整理する

質問の状況は、概ね次のような構成です。

  • DataGrid の ItemsSource に ObservableCollection<Messaggi> をバインド
  • 行データの COLOR 値(例:int)に応じて RowStyle の DataTrigger で背景色を変更
  • COLOR の意味は次の通り
COLOR 値見た目実装イメージ期待動作
1赤(固定)DataTrigger + Setter常に赤で安定
2緑(固定)DataTrigger + SetterACK で削除対象
3赤/黒点滅Storyboard / BeginStoryboardACK で点滅停止し、COLOR=1(赤固定)へ

そして実際の問題は次の2つです。

  • 点滅行を複数追加すると、行ごとに点滅開始タイミングがズレて同期しない
  • ACK で COLOR=3 → 1 に変更しても、点滅が止まらず赤固定に切り替わらない(見た目が更新されない)

結論:ハマりどころは「通知」と「アニメーション」

症状よくある原因効く対策推奨度
COLOR を変えたのに行色が変わらないMessaggi が INotifyPropertyChanged 未実装COLOR 変更時に PropertyChanged を通知★★★★★
COLOR=3→1 にしても点滅が止まらないStoryboard が Background を掴み続けている(優先順位)ExitActions で StopStoryboard / Remove を実装★★★★★
点滅が行ごとにズレる各行で Storyboard が別々のタイミングで BeginItems.Refresh で同期リセット / 点滅クロックを1本化回避:★★★ / 正攻法:★★★★★

問題1:COLOR を変更しても DataGrid の見た目が更新されない

原因の本命:ObservableCollection は「要素のプロパティ変更」を通知しない

ObservableCollection は “コレクションの変更(追加・削除)” は通知できますが、要素の中身(例:Messaggi.COLOR)の変更は通知しません。 そのため Messaggi 側が通知(INotifyPropertyChanged)を持っていないと、DataTrigger が再評価されず、見た目が切り替わりません。

正攻法:Messaggi に INotifyPropertyChanged を実装する

まずはモデル(または ViewModel)側を、WPF のバインディングが正しく追従できる形に整えます。

using System.ComponentModel;

public class Messaggi : INotifyPropertyChanged
{
    private int _color;

    public int COLOR
    {
        get => _color;
        set
        {
            if (_color == value) return;
            _color = value;
            OnPropertyChanged(nameof(COLOR));
        }
    }

    // 例:他の列も同様に通知させる
    // public string Message { get; set; }

    public event PropertyChangedEventHandler? PropertyChanged;

    protected virtual void OnPropertyChanged(string propertyName)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

これだけで、ACK で COLOR=3→1 の変更が入った瞬間に RowStyle の Trigger が再評価され、背景が切り替わるようになります(後述の「点滅が止まらない」問題も、通知が効いてはじめて ExitActions が動きます)。

応急処置:dg.Items.Refresh() で強制再描画する

「すぐ直したい」「既存コードの改修が大きい」などで、まずは表示を追従させたい場合は、ACK 処理の最後に DataGrid の再描画を入れる方法があります。

// ACK の最後など
dg.Items.Refresh();

ただしこれは根本解決ではありません。大量行や頻繁更新では負荷が上がりやすく、選択状態・スクロール位置・仮想化との相性によって副作用が出ることがあります。中長期運用では INotifyPropertyChanged を優先してください。

手段メリットデメリット使いどころ
INotifyPropertyChanged 実装自然にUI更新、MVVMとも相性良い、拡張しやすいモデル修正が必要推奨(ほぼ常に)
dg.Items.Refresh()手軽、即効性がある重い/副作用、更新頻度が高いと辛い応急処置、同期リセット目的

補足:UI スレッド以外から更新していないかも確認する

バックグラウンドスレッドでコレクションや要素を更新している場合、例外が出なくても UI が不安定になるケースがあります。少なくとも ItemsSource の更新や COLOR 変更を UI スレッドで行うのが安全です。

Application.Current.Dispatcher.Invoke(() =>
{
    // collection.Add(...)
    // item.COLOR = 1;
});

問題2:ACK で COLOR=3→1 にしても点滅が止まらない

原因:WPF ではアニメーションが「値の優先順位」で強い

WPF の依存プロパティは、同じプロパティに複数の「値の供給元」があると優先順位で勝敗が決まります。Storyboard によるアニメーションは優先順位が高く、Trigger の Setter で Background を赤に設定しても、アニメーションが生きている限り上書きされ続けます。

つまり、COLOR を 1 に変えて Trigger の Setter が働いても、点滅アニメーションを止めない限り“点滅が勝ち続ける” という状態になり得ます。

推奨:DataTrigger の EnterActions/ExitActions で Start/Stop を対にする

点滅の開始だけを入れて「終了(Stop)」を入れないと、COLOR が変わってもアニメーションが残ってしまうことがあります。Style の Trigger を次のように組むと、COLOR=3 の間だけ点滅し、外れた瞬間に確実に停止させやすくなります。

<DataGrid.RowStyle>
  <Style TargetType="{x:Type DataGridRow}">

    <!-- 既定(必要なら) -->
    <Setter Property="Background" Value="Black"/>

    <Style.Triggers>

      <!-- 赤固定 -->
      <DataTrigger Binding="{Binding COLOR}" Value="1">
        <Setter Property="Background" Value="Red"/>
      </DataTrigger>

      <!-- 緑固定 -->
      <DataTrigger Binding="{Binding COLOR}" Value="2">
        <Setter Property="Background" Value="LimeGreen"/>
      </DataTrigger>

      <!-- 点滅(赤/黒) -->
      <DataTrigger Binding="{Binding COLOR}" Value="3">

        <!-- 点滅時は SolidColorBrush を前提にしておく(色アニメのターゲットを安定させる) -->
        <Setter Property="Background">
          <Setter.Value>
            <SolidColorBrush Color="Black"/>
          </Setter.Value>
        </Setter>

        <DataTrigger.EnterActions>
          <BeginStoryboard x:Name="BlinkBegin" HandoffBehavior="SnapshotAndReplace">
            <Storyboard RepeatBehavior="Forever" AutoReverse="True">
              <ColorAnimation
                Storyboard.TargetProperty="(Control.Background).(SolidColorBrush.Color)"
                To="Red"
                Duration="0:0:0.5"/>
            </Storyboard>
          </BeginStoryboard>
        </DataTrigger.EnterActions>

        <DataTrigger.ExitActions>
          <StopStoryboard BeginStoryboardName="BlinkBegin"/>
        </DataTrigger.ExitActions>

      </DataTrigger>

    </Style.Triggers>
  </Style>
</DataGrid.RowStyle>

ポイントは次の3つです。

  • EnterActions で BeginStoryboard、ExitActions で StopStoryboard を必ずセットにする
  • Background は Brush なので、色アニメを安定させるため SolidColorBrush を明示してから ColorAnimation する
  • 複数回 Begin される可能性に備え、HandoffBehavior=”SnapshotAndReplace” で重ね掛けを避ける

そして大前提として、ExitActions を動かすには、COLOR 変更が UI に伝わる必要があります。つまり INotifyPropertyChanged(もしくは Items.Refresh) が前提になります。

追加の落とし穴:仮想化(Virtualization)で「点滅が残る」ケース

DataGrid はパフォーマンスのために行コンテナ(DataGridRow)を再利用することがあります。点滅開始を Loaded イベントなどで行っている場合、行が再利用されると、別アイテムに差し替わったのにアニメーションだけ残る といった現象に繋がります。

この意味でも、Loaded/Unloaded での開始停止より、DataTrigger の Enter/Exit で開始停止を管理する方が安全です(DataContext の変化に追従しやすいため)。

問題3:点滅開始タイミングが行ごとにズレて同期しない

なぜズレるのか:行コンテナ生成タイミング=点滅開始タイミング

各行に Storyboard を持たせる設計だと、点滅は「その行が画面に現れた瞬間(行コンテナが生成/適用された瞬間)」から始まります。ボタンで行を追加するタイミングが違えば、当然 Storyboard の Begin のタイミングもズレます。結果として、点滅が同期しません。

方法A:簡単に揃える(リセットで同期)

手軽に同期させるなら、追加後に dg.Items.Refresh() を呼んで行コンテナを作り直し、点滅を同時に始め直す方法があります。質問にある「Refresh で同期しやすい」という現象はこの効果です。

// 追加ボタン後など(応急処置)
dg.Items.Refresh();

ただし、これも応急処置です。行数が増えるほどコストが高くなり、頻繁に呼ぶと体感が悪くなります。また、選択や並び替え中などに呼ぶと副作用が出ることがあります。

方法B:確実に揃える(点滅を1本化して“同じクロック”を共有する)

同期を本気で取りに行くなら、点滅のタイムラインを “各行で個別に持つ” のをやめ、点滅を1本だけ再生して、それを各行が参照する方式が強いです。実装の考え方は次の通りです。

  • 画面に “見えない点滅ソース” を1つ用意(例:Visibility=”Collapsed” の Border)
  • その Background を Storyboard で点滅させる(ここだけが点滅クロックを持つ)
  • COLOR=3 の行は、その見えない点滅ソースの Background を参照(バインド)して表示する

これなら、行を何回追加しても、参照しているのは同じ Background なので、全行が必ず同位相で点滅します。

点滅クロック1本化の XAML 例(見えない Border を使う)

以下は、同一画面内(同じ XAML の namescope)で完結させる例です。重要なのは、RowStyle から ElementName で参照できる場所に点滅ソースを置くことです(App.xaml など別スコープに置くと参照できないことがあります)。

<Grid>

  <!-- 点滅クロック(見えない) -->
  <Border x:Name="BlinkSource" Visibility="Collapsed">
    <Border.Background>
      <SolidColorBrush Color="Black"/>
    </Border.Background>
    <Border.Triggers>
      <EventTrigger RoutedEvent="FrameworkElement.Loaded">
        <BeginStoryboard>
          <Storyboard RepeatBehavior="Forever" AutoReverse="True">
            <ColorAnimation
              Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
              To="Red"
              Duration="0:0:0.5"/>
          </Storyboard>
        </BeginStoryboard>
      </EventTrigger>
    </Border.Triggers>
  </Border>

  <DataGrid x:Name="dg" ItemsSource="{Binding MessaggiList}">
    <DataGrid.RowStyle>
      <Style TargetType="{x:Type DataGridRow}">
        <Style.Triggers>

          <DataTrigger Binding="{Binding COLOR}" Value="1">
            <Setter Property="Background" Value="Red"/>
          </DataTrigger>

          <DataTrigger Binding="{Binding COLOR}" Value="2">
            <Setter Property="Background" Value="LimeGreen"/>
          </DataTrigger>

          <DataTrigger Binding="{Binding COLOR}" Value="3">
            <Setter Property="Background"
                    Value="{Binding ElementName=BlinkSource, Path=Background}"/>
          </DataTrigger>

        </Style.Triggers>
      </Style>
    </DataGrid.RowStyle>
  </DataGrid>

</Grid>

この方式の利点は、COLOR=3 の行は “点滅ソースを参照しているだけ” なので、ACK で COLOR=1 に変えた瞬間に参照が外れ、点滅を止める処理が不要になる点です(もちろん、COLOR 変更通知は必要です)。

同期方式同期精度実装難易度注意点
Items.Refresh で同期リセットそこそこ低重い/副作用、頻繁更新に不向き
各行 Storyboard(個別)低(ズレる)中追加タイミングで必ず位相差が出る
点滅クロック1本化(共有)高(完全同期)中ElementName の参照スコープに注意

ACK 処理の実装例:削除と COLOR 変更を安全に行う

ACK ボタンで「COLOR=2 の行は削除」「COLOR=3 の行は COLOR=1 に変更」を行うとき、ObservableCollection を foreach で回しながら Remove すると例外になります。安全なやり方を紹介します。

パターン1:後ろから for で削除する(定番)

private void Ack()
{
    // MessaggiList : ObservableCollection<Messaggi>


for (int i = MessaggiList.Count - 1; i >= 0; i--)
{
    var item = MessaggiList[i];

    if (item.COLOR == 2)
    {
        MessaggiList.RemoveAt(i);
        continue;
    }

    if (item.COLOR == 3)
    {
        item.COLOR = 1; // INotifyPropertyChanged があれば UI も追従
    }
}

// 応急処置として使うなら最後に
// dg.Items.Refresh();


}

パターン2:削除対象だけ List 化してから Remove(読みやすい)

private void Ack()
{
    var removeTargets = MessaggiList.Where(x => x.COLOR == 2).ToList();
    foreach (var x in removeTargets)
    {
        MessaggiList.Remove(x);
    }


foreach (var x in MessaggiList)
{
    if (x.COLOR == 3)
        x.COLOR = 1;
}

// dg.Items.Refresh(); // 応急処置


}

行数が多く更新頻度が高い場合は、削除や更新をまとめて行い、必要なら View 側の Refresh を最小限にする設計(INotifyPropertyChanged を徹底する、更新をバッチ化する、など)が効いてきます。

点滅が「点滅していないように見える」場合の調整

点滅の Storyboard が動いていても、KeyTime や Duration が長すぎると、見た目として点滅していないように感じます。特に 00:01:00(1分) のような指定は、トラブルシュート中は長すぎます。

目的おすすめ設定理由
動作確認(分かりやすさ重視)0.3〜0.7秒目視で点滅がすぐ分かる
運用(過度にチカチカさせない)0.8〜1.5秒注意喚起しつつ疲れにくい
強いアラート0.2〜0.4秒緊急度を上げたいとき

また、赤と黒のコントラストが強すぎると視認性は高い一方で疲れやすいので、運用では黒を濃いグレーにするなどの調整も有効です(色の方針は現場の UI ガイドラインに合わせてください)。

チェックリスト:直ったつもりでも再発するポイント

  • COLOR の setter で PropertyChanged が発火しているか(INotifyPropertyChanged)
  • 点滅は Begin だけでなく Stop/Exit を用意しているか(個別 Storyboard の場合)
  • Background を ColorAnimation するなら、SolidColorBrush を前提にしているか
  • RowStyle を App.xaml 側に置いて ElementName 参照できず、共有点滅方式が効いていない状態になっていないか
  • 仮想化で行が再利用され、アニメーションが残留していないか(Loaded 依存の実装は要注意)
  • 更新処理が UI スレッド外で走っていないか(Dispatcher を使っているか)

おすすめの落としどころ:現場で困りにくい構成

実運用で「壊れにくい」「説明しやすい」構成は次の組み合わせです。

  • Messaggi に INotifyPropertyChanged を実装(COLOR 変更を確実に通知)
  • 点滅は、要件が「同期必須」なら 点滅クロック1本化(共有)
  • 同期が不要で、かつ行ごとに制御したいなら DataTrigger Enter/Exit で Begin/Stop
  • dg.Items.Refresh() は最後の手段(応急処置・同期リセット用途に限定)

特に「ACK で COLOR=3→1 にしたのに点滅が残る」は、通知不足(Trigger が再評価されない)とアニメーションが強い(止めない限り勝ち続ける)が同時に起きているケースが多いので、片方だけ直しても直り切りません。今回の2点(INotifyPropertyChanged+停止設計、または共有点滅)をセットで押さえると安定します。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次