PrismのUpdateTextBindingOnPropertyChangedをDataGridのCellEditingTemplateで使うと例外が・・・

今日ちょこっとTweetしたんですが、
PrismのUpdateTextBindingOnPropertyChangedビヘイビアーをDataGridのCellEditingTemplateで使うとInvalidOperationExceptionが発生するようでしたのでPrismのCodePlexにIssueとして投稿しておきました。
■Issue #8864: UpdateTextBindingOnPropertyChanged Behavior with DataGridTemplateColumn.CellEditingTemplate cause InvalidOperationException

再現ソースも添付ファイルとしてあげました。
どうも、DataGrid内のTextBoxが完全に現れる前にGetBindingExpressionしちゃうと良くないようです。
微妙ですね。

というか、Silverlight5だと「UpdateSourceTrigger=PropertyChanged」が使えるようになるのでこのビヘイビアーはまったく要らなくなるんですけどね・・・。
ああもうSilverlight5早く出てちょうだい!

昨日のサンプルが一部間違っていたので修正

昨日の記事、
■フォーラムの質問にReactivePropertyサンプルで答える
で、一部おかしな部分があったのを朝、id:neueccさんにご指摘いただきました。

aaa.IsChecked.ObserveProperty(x => x.Value).Where(x => x).Subscribe(x => Items2.Add(aaa));

の部分、
もともとIsCheckedプロパティはReactivePropertyなので、ObservePropertyメソッドを使わなくてもそのままSubscribeが呼べます。

aaa.IsChecked.Where(x => x).Subscribe(x => Items2.Add(aaa));

こう書くべきでした。
ObservePropertyメソッドは元々、普通の通知付きModelを変換するためのもので、ReactivePropertyになっているならそのままSubscribeが呼べます、っていうかそのままSubscribeするためにReactivePropertyがあるのに・・・。

眠気の中でうっかりしていました。
id:neueccさん、ありがとうございました。

フォーラムの質問にReactivePropertyサンプルで答える

id:neueccさんが作ったReactivePropertyというライブラリが素敵すぎて悶え死にそうな今日この頃、
たまたまMSDNフォーラムでReactiveProperty使えそうな質問があったのでサンプル作ってみた。

MSDNフォーラムの記事↓
■CheckBox付きのListBoxでチェックの付いている項目だけを別のListBoxに表示するには?
http://social.msdn.microsoft.com/Forums/ja-JP/wpfja/thread/ead88361-cca8-4bc9-b495-a7822d851de9

2つのListBoxを連動させたいってもの。

↓作ったサンプルはこれ↓
https://skydrive.live.com/?cid=bc790e45968a1da9&sc=documents&uc=1&id=BC790E45968A1DA9%21109#

やっぱり、ReactivePropertyの何が凄いって、Property毎にObserve(観察)できるとこ(他にも沢山あるけど)

ちなみに書いたソースはこんだけ

    class MainViewModel
    {
        public ReactiveCollection<Item> Items1 { get; private set; }
        public ReactiveCollection<Item> Items2 { get; private set; }
        public ReactiveProperty<bool> IsVisible { get; private set; }

        public MainViewModel()
        {
            var aaa = new Item("aaa");
            var bbb = new Item("bbb");
            var ccc = new Item("ccc");

            Items1 = new Item[] { aaa, bbb, ccc }.ToObservable().ToReactiveCollection();
            Items2 = new ReactiveCollection<Item>();

            //aaa.IsChecked.ObserveProperty(x => x.Value).Where(x => x).Subscribe(x => Items2.Add(aaa));
            //aaa.IsChecked.ObserveProperty(x => x.Value).Where(x => !x).Subscribe(x => Items2.Remove(aaa));

            //bbb.IsChecked.ObserveProperty(x => x.Value).Where(x => x).Subscribe(x => Items2.Add(bbb));
            //bbb.IsChecked.ObserveProperty(x => x.Value).Where(x => !x).Subscribe(x => Items2.Remove(bbb));

            //ccc.IsChecked.ObserveProperty(x => x.Value).Where(x => x).Subscribe(x => Items2.Add(ccc));
            //ccc.IsChecked.ObserveProperty(x => x.Value).Where(x => !x).Subscribe(x => Items2.Remove(ccc));

            /* 2011.10.21 */
            /* neueさんのご指摘で気づきました、「ObserveProperty」は不要でしたので修正します。 */

            aaa.IsChecked.Where(x => x).Subscribe(x => Items2.Add(aaa));
            aaa.IsChecked.Where(x => !x).Subscribe(x => Items2.Remove(aaa));

            bbb.IsChecked.Where(x => x).Subscribe(x => Items2.Add(bbb));
            bbb.IsChecked.Where(x => !x).Subscribe(x => Items2.Remove(bbb));

            ccc.IsChecked.Where(x => x).Subscribe(x => Items2.Add(ccc));
            ccc.IsChecked.Where(x => !x).Subscribe(x => Items2.Remove(ccc));

            IsVisible = new ReactiveProperty<bool>();

            var cmd = Items2.CollectionChangedAsObservable().Subscribe(_ => IsVisible.Value = Items2.Count > 0);
        }
    }

    class Item
    {
        public ReactiveProperty<bool> IsChecked { get; private set; }

        public ReactiveProperty<string> Text { get; private set; }

        public Item(string text)
        {
            IsChecked = new ReactiveProperty<bool>();
            Text = new ReactiveProperty<string>(text);
        }
    }

こんくらいのサンプルだとPrismもINotifyPropertyChangedもいらない。
RaisePropertyChangedもいらない。
PropertyChangedEventArgsとってプロパティ名調べてなんてこともいらない。

MVVMもこんなに簡単。
そう、ReactivePropertyならね!

DataGridの行の背景色を行データによって変えたいのよ

SilverlightのDataGridは主にItemsSourceにデータをバインドして使うと思うけど
今日はそのデータによって行の背景色を変えてみましょうという記事です。

よくあるケースで考えられるのは
「データが変更されている場合はピンク色で」とか、
「ユーザーが(エラーじゃ無いけど)斜め上な値を入力したら警告の意味を含めて赤色で」
とか。

このやり方、いろいろGoogle先生に聞いてみたんだけど、
DataGrid.LoadingRowイベントのハンドラで

e.Row.BackGround = 〜〜

みたいにするのが多いみたい。
でも、これだと行が描画されるときにしか色が変わらない。
そうじゃなくて、「編集された時点でピンクになる」とか、リアルタイムで変わって欲しいときのサンプルが無いようだったので書きます。
「状態によって見た目が変化」なので、VisualStateを使ってみようと思います。

※ Expression Blendが必須です。(Silverlight開発には必須ですよ〜)

例えば、こんなDataGridがあったとします。

こいつはWCF RIA Servicesを使って「データソース」ウインドウからポトペタでページに貼っ付けただけのものです。
簡単ですねぇ。
せっかくEntityが行にバインドされているので、こいつのEntityState(変更されたかどうか)によって色が変わるようにしてみましょう。

いざ。

1.RowStyleの編集からテンプレートを開く

Expression Blendで該当データグリッドを右クリック → 「追加テンプレートの編集」 → 「RowStyleの編集」
と選択していき、DataGridのRowStyleを編集します。

「オブジェクトとタイムライン」ウインドウには、こんな感じでRowのテンプレートが丸裸にされます。

2.まずはCellsPresenterのBackGroundをTransParentに設定

テンプレートの中の「CellsPresenter」というコントロールがあります。
こいつはその名の通り行中のCell達を表示するコンテナです。
こいつのBackgroundを弄ることによって、行の背景色を変化させることにします。
行データのEntityStateが変更されたときだけ色をつけたいので、基本はTransParentとしておきます。

3.新しいVisualStateGroup、VisualStateを作る

左上の「状態」タブで、新しいVisualStateGroupを作成します。

既に「MouseOver」とか色々状態はありますが、今回はEntityの状態によって変化させるので専用の状態グループと状態を作ります。

グループ名は「EntityStates」
状態は「Modified」と「Unmodified」にしてみました。

4.状態「Modified」を記録する

さっき作った状態「Modified」の記録モードをONにして、CellsPresenterの背景色を変更します。

今回はピンクにしてみました。
Opacityを100%にしちゃうと行が選択された時とかわかりにくくなるので微妙に薄めておきます。(70%にしてます)

また、「Unmodified」の方は何も記録しません。(通常行なので当たり前ですが)

5.GoToStateActionを仕込んでできあがり

ここまでで状態の設定は終わりました。
しかし、一体誰がこの状態に持って行けば良いでしょう?

そこでGoToStateActionの登場です。

「アセット」タブから、GoToStateActionを選択し、テンプレートの「Root」直下に配置します。

配置したら、右側のプロパティペインでGoToStateActionの設定をします。
まずは「Modified」の設定。

ポイントは、

  • TriggerActionは既定でEventTriggerに結びつくけど、今回はイベントでは無くデータで変化するのでDataTriggerに換える。
  • Bindingはカスタム式でEntityStateを指定(行にバインドしているEntity.EntityStateによって背景色を変化させるため)
  • StateNameは上記で作った「Modified」を指定

また、同じように「Unmodified」も設定します。

さあ、出来上がりです。
プロジェクトを実行すると・・・


名前を編集して確定すると色が・・・

変わったーーー!!!!
さあっし。

ちなみに、今回はWCF RIA Servicesを使用しているので、ViewModelとかで

personDomainDataSource.RejectChanges();

とかやるとちゃんと元の色に戻ってくれます。

まとめ

今回紹介したやり方だと、1行もC#コードを書かずにデータによってDataGridの行の背景色を変化させることが可能だということがわかります。
Expression Blendすばらしい・・・。
私はもうBlend無しにSilverlight開発はできなくなってます・・・。

ちなみに、今回の作業でBlendが吐き出したXamlの重要な部分を見ておくと、

<Style x:Key="DataGridRowStyle1" TargetType="sdk:DataGridRow">
  <Setter Property="IsTabStop" Value="False"/>
  <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="sdk:DataGridRow">
          <sdk:DataGridFrozenGrid x:Name="Root">
          
              <VisualStateManager.VisualStateGroups>
                <VisualStateGroup x:Name="EntityStates">
                  <VisualState x:Name="Modified">
                      <Storyboard>
                        <ColorAnimation Duration="0" To="Pink" Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)" Storyboard.TargetName="CellsPresenter" d:IsOptimized="True"/>
                        <DoubleAnimation Duration="0" To="0.7" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="CellsPresenter" d:IsOptimized="True"/>
                      </Storyboard>
                  </VisualState>
                  <VisualState x:Name="Unmodified"/>
                </VisualStateGroup>
              </VisualStateManager.VisualStateGroups>
              
              <i:Interaction.Triggers>
                <ei:DataTrigger Binding="{Binding EntityState}" Value="Modified">
                  <ei:GoToStateAction StateName="Modified"/>
                </ei:DataTrigger>
                <ei:DataTrigger Binding="{Binding EntityState}" Value="Unmodified">
                  <ei:GoToStateAction StateName="Unmodified"/>
                </ei:DataTrigger>
              </i:Interaction.Triggers>

という風になってます。
状態毎に背景を変えて、
その状態を呼び出すDataTriggerとActionがあって・・・
Xamlだとすごくわかりやすいですね。

ということで、ビバ! Blend!

DataGridのCellTemplateが親のDataContextにバインドするのって以外に難しくね?

Pageの中にDataGridが1つあったとします。
こんなかんじで。

  • Page    (DataContext=PageViewModel)
    • DataGrid  (ItemsSource=PageViewModel.DataGridItems)
      • DataGridTemplateColumn
        • DataGridTemplateColumn.CellTemplate
          • ComboBox

さて、ここで問題。

CellTemplateの中のComboBoxのItemsSouceに、PageViewModelのプロパティを使いたい場合、どうすればいいでしょうか。

...
即答されると悲しいけど、以外に難しいと思う。
直感的には

<ComboBox ItemsSource="{Binding DataContext.ComboSource, ElementName=LayoutRoot}" />
<!-- ※これは動きません --> 

とか、

<ComboBox ItemsSource="{Binding ComboSource, Source=Parent}" />
<!-- ※これは大嘘コードです --> 

とか書けると良いけど、残念ながらダメ。

結構、こうしたい局面ってあると思う。
Pageで大項目を指定させて、DataGridのすべての行は、それに紐尽く小項目をItemsSourceにしたいとか。
(Pageが都道府県で、行が市、ってなんかそう。)

解決策でよく見かける(自分もやっていた)は

  1. 行アイテムにも行アイテム用のViewModelを持たせ、PageViewModelとの連携を図る
  2. ページのリソースにPageViewModelを定義し、そこを参照する

例:

    <UserControl.Resources>
        <local:PageViewModel x:Key="PageViewModel" />
    </UserControl.Resources>

    <Grid x:Name="LayoutRoot" DataContext="{StaticResource PageViewModel}">

        <sdk:DataGrid>
            ...........
            <ComboBox ItemsSource="{Binding ComboSource, Source={StaticResource PageViewModel}}" />

とかです。
でも、それぞれ難点があって、
1は、王道な解決策なんだろうけど、WCF RIA Services使ってると行用のViewModelなんか絶対作りたくない(利点をまるまる殺してしまうから)と思うし、
2はPageのDataContextにViewModelを設定できないのがイケてない・・・。
......
簡単そうに見えて、世の中甘くないもんですね。。。
......

と拗ねてたら、かなり小手先ではあるけど1つやり方を考えついた。
注目したのは

  • ResouceはTemplateの中からもアクセスできる。
  • Resouceに追加されたオブジェクトは、PageViewModelのプロパティにバインドできる。

の2点。

2つ合わせて、中継用オブジェクトをResouceに追加しちゃえば良いじゃない。

では、そのやり方。

1.まず、中継用クラスを作ります。

    public class RelayPropertyObject : DependencyObject
    {
        /// <summary>
        /// 何でも中継しちゃうのでObjectです。
        /// </summary>
        public object RelayProperty
        {
            get { return (object)GetValue(RelayPropertyProperty); }
            set { SetValue(RelayPropertyProperty, value); }
        }

        public static readonly DependencyProperty RelayPropertyProperty =
            DependencyProperty.Register(
            "RelayProperty", typeof(object), typeof(RelayPropertyObject), new PropertyMetadata(null));
    }

簡単ですねえ。

2.そしたら、Pageのリソースに突っ込みます。

    <UserControl.Resources>
        <local:RelayPropertyObject x:Key="RelayPropertyObject" RelayProperty="{Binding ComboSource}" />
    </UserControl.Resources>

RelayPropertyにPageViewModelのプロパティをバインドしているのがミソです。

3.CellTemplateは

 <ComboBox ItemsSource="{Binding RelayProperty, Source={StaticResource RelayPropertyObject}}" />

のように、ResouceにあるRelayPropertyObjectのRelayPropertyを指定します。
 
これでできちゃいます。

メデタシメデタシ。(静止画じゃようわからんですね・・・

ちなみにこのやり方、便利なのはCommandもバインドできちゃいます。(あたりまえか)

    <UserControl.Resources>
        <local:RelayPropertyObject x:Key="RelayPropertyObject" RelayProperty="{Binding ComboSource}" />
        <local:RelayPropertyObject x:Key="RemoveRowCommandRelay" RelayProperty="{Binding RemoveRowCommand}" />
    </UserControl.Resources>

.......................
    <DataTemplate>
        <ComboBox ItemsSource="{Binding RelayProperty, Source={StaticResource RelayPropertyObject}}" />
    </DataTemplate>
........................
    <DataTemplate>
        <Button Command="{Binding RelayProperty, Source={StaticResource RemoveRowCommandRelay}}" 
                  CommandParameter="{Binding RowId}" />
    </DataTemplate>

    <!-- 2011.10.07 Xaml記述が間違っていたので修正しました。 -->

なんて書いちゃって、行削除ボタンが作れたりします。
すごいですねぇ。うれしいですねぇ。

でも、やっぱり最初に書いた”Souce=Parent”とかが一番直感的なので、そう書かして欲しいですねぇ。

Entity Framework4.1をWCF RIA Servicesで使う手順はこれで良いかな

久々のブログ(ブログというか月報になっとるけど)。

WCF RIA Servicesを使おうかと考えているシステムがあって、じゃあどうせならEntity Frameworkも最新の4.1を使いたいなあと思ったんだけど、これがちょっと難儀した。
EF4.0だったら項目の追加からドメインサービスを追加したら大体あんじょうよろしくやってくれるんだけど、4.1は新しいせいかいろいろと手作業しなければならない感じだった。

忘れないように手順を残しておこうと思うのと、調べ足りないだけでもっと良い方法がある場合は指摘してもらえると有り難いなあと思ってEntity Framework4.1 を WCF RIA Services で使う手順を書きます。
今回はデータベースファーストでやりましたが、コードファーストでもだいたい同じです。

1.まずはプロジェクト作成

ふつうのSilverlightアプリケーションプロジェクトを作成します。
RIA サービスは有効に。

2.NuGetでRIAServices.EntityFrameworkをゲット

NuGet Galleryはこちら
http://nuget.org/List/Packages/RIAServices.EntityFramework

3.データモデルを作成

EntityFramewok4.0と同じ手順でデータモデルを作成します。
今回はデータベースファースト

至極シンプルなデータベース・・・

データはこんな感じ

4.ドメインサービスクラスを追加

ドメインサービスクラスを追加します。

この段階ではEF4.1はまだ使われていません。EF4.0用のドメインサービスクラスが出来上がります。

5.DbContext Generatorを使ってEF4.1用のEntityクラスを生成

データモデルのデザイナ上で右クリックして、「コード生成項目の追加」を選択します。

コードジェネレーターの選択ダイアログが出るので、オンラインテンプレートの「ADO.NET C# DbContext Generator」を選択します。

こでれ、EntityクラスがEF4.1を使用したものになると同時に、ドメインサービスクラスが4.1に対応していないためビルドエラーとなります。

6.ドメインサービスクラスをEF4.1用に修正

現段階だとビルドが通らないので、4で追加したドメインサービスクラスをEF4.1用に書き換えます。

コード例 (今回の例はGetだけ。更新系はまた別の機会にやろうと思います。)

   [EnableClientAccess()]
    public class SampleService : DbDomainService<SampleDBEntities>
    {
        public IQueryable<Country> GetCountries()
        {
            return this.DbContext.Countries;
        }

        public IQueryable<Driver> GetDrivers()
        {
            return this.DbContext.Drivers.Include("Countries");
        }
    }

ポイントは3つ。

    • LinqToEntitiesDomainServiceではなくDbDomainServiceクラスを継承させる。
    • ObjectContextではなくDbContextを使う。
    • 関連エンティティを取得するためにInclude()メソッドを使う。

7.Entityクラス生成テンプレートを修正する

関連エンティティを取得するためにEntityクラスのナビゲーションプロパティに[Include]属性をつけます。
テンプレートで自動生成されるクラスなので、テンプレート(ttファイル)の方を修正します。
下記2カ所です。

  • 197行目くらいの名前空間を書き込むところに下記を追加
using System.ServiceModel.DomainServices.Server;
  • 237行目くらいのWriteNavigationPropertyメソッドの最初に下記を追加
#>
	[Include]
<#+

そうすると自動生成されたクラスがこんな感じになります。

ちゃんと[Include]属性が付いてます。
これで、サーバー側の作業は終わりです。(ふぅ・・・)

8.クライアント側からの利用方法

クライアント用のDomainContextを生成して、Loadメソッドを使って取得します。
画面とかの細かいことは下の方にサンプルソースのリンクを張りますのでそちらを見てください。

    //サービスに対応するDomainContextを生成します。
    var context = new SampleContext();

    //Driversを取得します。
    context.Load(
        context.GetDriversQuery(),
        loadOperation => 
        {
            Drivers = new ObservableCollection<Driver>(loadOperation.Entities);
        },
        null);

はい、ちゃんとCountriesエンティティのCountryName(国名)も取得できているようです。

9.まとめ

とりあえず、このやり方でEntity Framework4.1でWCF RIA Servicesが使えそう。
ただ、実はもっと簡単なやり方があるんじゃないかと思ったりもする。みんなどうやってるんだろう・・・。

とりあえずドメインサービスクラスのテンプレートがEF4.1対応してくれたらなあと思う。
来年あたりになればこんなことしなくて良いようになるんだろうけど。

「実はこの設定でやればできるよ」
とかいうのが最近多いので、探し切れていない可能性も。
もしもっと簡単なやり方ご存じの方、教えてください・・・。

とここまで書いたところでこの記事を発見
WCF RIA Services Support for EF 4.1 (and EF Code-First) « varunpuranik

Again, the SP2 release bits will have full wizard support for DbContext. So you should not need to do any of this dance with the code snippet. But till then, this should be easy enough for you to do.

やっぱSP2で対応なのよねん。寿命の短い記事になりそうです・・・

あ、サンプルソースはこちらです。(SQL CE4.0使用)
http://ef41riaservicessampl.codeplex.com/SourceControl/changeset/changes/10399

WeakEventパターンだからって安心しきっててRemoveListenerしないと痛い目みる

前回(MVVMパターンで陥りやすいメモリリークについて考えてみた)の続きっぽいこと書きます。

前回ちょっとだけ触れたWeakEventパターンについてもう少し。
※このパターン自体についての詳しい説明はないのでリンク先を見てください。WeakEventでググってきた方すみません。この記事はWeakEventパターンを実装してもオブジェクトの生存期間については意識しとかないと行けないという初歩的なことを書いてます。

僕がこのパターンをはじめに知ったとき

これはすごい!
これで好きなようにイベント購読してもどこから参照残ってるかなんて気にしなくていいじゃん!
もう今度から+=とか−=とか使うのやめようぜ! 全部WeakEventにしようぜ!
さよならメモリリーク! ビバ! WeakEventパターン!

とか思った。

しかし・・・

人生そんなに甘くなかった。
甘いのは俺だった。

なぜか。
WeakEventパターンは、簡単に言ってしまえば
イベントリスナーが生きている場合のみイベントソースはイベントを通知するという仕組みだけど、
この「生きている場合のみ」がクセモノだった。

「生きている場合」ってどうゆうこと?

Garbage CollecterにCollectされていないこと。

え、Collectっていつされるんだったっけ?

当然ですが、自分でGC.Collect()メソッド呼ばなければ、Garbage Collecterが最適な時点を適切に判断して行ってくれます。
ガベージ コレクションの基礎

最適な時点を適切に判断って、国会答弁みたいだな(冗談)
つまるところ、GC.Collect()を呼ばない限りいつ実行されるかはプログラマは判断できないってこと。

では、どういうことが起こるか。

WeakEventパターンだからと安心しきってリスナー登録を解除しないと、タイミングによっては死んだと思っていたリスナーが生きていて、ハンドラーが呼び出されてしまうことがあり得るってこと。

死んだはずだよ、お富さん。。。

死にゆくオブジェクトのプロパティが変わるくらいなら気にしなくて良いのかもしれないけど、例えばデータを更新したりするようなことをハンドラで行っていた場合、タイミングによっては致命的なバグになるんじゃあ・・・。

バグにならないとしてもGarbageがいつCollectされるかが定かではない以上、呼ばれるか呼ばれないかを制御できていないことになるのはちょっと気持ち悪い。
じゃあ毎回GC.Collect()をコードで呼ぶ?・・・それも「ガベージ コレクションの強制実行」のガイドラインに反している気がする・・・。(古いドキュメントだけど有効だと思う)

結局、プログラムの方はリスナーがいつ消えて欲しいかわかるわけ無いんだから、消えて欲しいのなら人間がちゃんと「消えて下さい」って命令してあげなきゃ行けない。
つまり、RemoveListenerメソッドを呼んであげなければならない。
普通に考えると「そりゃそうだ」なんだけど、「WeakEventにしてるからイベントハンドラ解除はしなくても安心」なんて考えちゃってると、大変。
要は強参照だろうが弱参照だろうがイベントハンドラ(リスナー)が必要なくなった時点で解除をしないといけない。
うーん。勉強になった。めでたしめでたし。

ちょと待て!
そもそもWeakEventパターンはいつ登録を解除するかを明示的には認識できない場合に使用するもんじゃないの?
登録を解除する時期がわかってるならWeakEventパターンなぞ使わなくて良いじゃない・・・。

元も子もない結論になりそうだったのでもう1度WeakEventパターンのMSDNライブラリをよーく読む。

引用:
・弱いイベント パターンの実装は、主にコントロール作成者にとって意味があります。コントロールの作成者には、作成したコントロールの動作と格納およびそれを組み込むアプリケーションに与える影響に対する主な責任があります。 これには、コントロール オブジェクトの有効期間の動作 (特に、上に示したメモリ リークの問題への対応) が含まれます。
・弱いイベント パターンの有意義な 1 つの特徴は、(あなたの*1)コード ベースの一部でないイベントに対してパターンを実装できることです。

ふーむ。
このライブラリの記事のかかれ方だと、WeakEventはかなり限定的な局面を想定しているようにも思える。
イベントソースに対して制御できる状況にない、コントロール作者とか、フレームワークのコアな部分とか。
逆に言うと何でもかんでもWeakEventパターンにするんではなく、イベントソースもイベントリスナーも完全に管理できる場合は普通の(強参照)のイベントパターンを使えということかな。
(あとパフォーマンス的にも強参照の方が良いしね)
イベントソースを管理できないコアな場面でWeakEventパターンを使用する場合も、リスナー登録解除の道は実装しておくべきだろうとは思う。
(たとえば、イベントソースが変更されたらきちんとRemoveListenerするとか、CloseやDisposeメソッドという名前で外から解除できるようにしておくとか)

繰り返し強調したいのは、
決してWeakEventパターンが不備なのではなく、WeakEventパターンさえ実装すれば万事OKと考えることがよくないということ。
常にオブジェクトの生存期間については意識しとかなきゃなあということ。
(WeakEventパターンさえ実装すれば万事OKなんて誰も考えていないかもだけど)

ちなみに、イベントに限らず弱参照全般についてはここにこんな記述もあった。
■弱い参照

引用:
弱い参照を使用するに当たってのガイドライン
・メモリ管理の問題の自動的な解決方法として、弱い参照を使用しないでください。 代わりに、アプリケーションのオブジェクトを処理するための効果的なキャッシュ ポリシーを作成します。

まとめ

  • WeakEventパターンもできるだけRemoveListenerすること。
  • いつ登録を解除するかを認識できないイベントはWeakEventパターンで。
  • 認識できるものは+=−=で。

買えるモノはMaster Cardで。

*1:抜けている様だったので補足しました