2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

WPF/Prism のViewModel にInterceptor を組み込む

Last updated at Posted at 2021-06-07

WPFでPrism 使うと ViewModel は自動紐づけしてくれるけど、そのままだとメソッド単位のトレースとか自前でやる必要がある。そうするとメソッド本体のロジックに無関係の物が紛れてしまうし、早期リターンとかするたびに logger.Trace("exit") と書き続けなければならないので少々面倒。

そこで ViewModel も Interceptor注入対象にしてトレースやらトランザクションやらを勝手に組み込めるようにしたい。

ここでは .NET Framework 4.6.1 + Prism.WPF 6.3 が対象。

パッケージ

  • Prism.Autofac.Wpf 6.3.0
  • Autofac 4.8.1
  • Autofac.Extras.DynamicProxy 4.5.0
  • NLog 4.5.11
  • ReactiveProperty 5.2.0

Interceptor

単純にメソッド実行前後でトレースをしゅうWPFでPrism 使うと ViewModel は自動紐づけしてくれるけど、そのままだとメソッド単位のトレースとか自前でやる必要がある。そうするとメソッド本体のロジックに無関係の物が紛れてしまうし、早期リターンとかするたびに logger.Trace("exit") と書き続けなければならないので少々面倒。

そこで ViewModel も Interceptor注入対象にしてトレースやらトランザクションやらを勝手に組み込めるようにしたい。

ここでは .NET Framework 4.6.1 + Prism.WPF 6.3 が対象。

パッケージ

  • Prism.Autofac.Wpf 6.3.0
  • Autofac 4.8.1
  • Autofac.Extras.DynamicProxy 4.5.0
  • NLog 4.5.11
  • ReactiveProperty 5.2.0

Interceptor

単純にメソッド実行前後でトレースを収集するような Interceptor

using Castle.DynamicProxy;
using System;
using System.Collections.Generic;
using System.Text;
using NLog;
using System.Diagnostics;
using System.Reflection;
namespace VmInjectionSample1.ComponentManagement
{
    public class TraceInterceptor : IInterceptor
    {
        public TraceInterceptor()
        {
        }
        public void Intercept(IInvocation invocation)
        {
            var logger = LogManager.GetLogger(invocation.TargetType.FullName);
            logger.Trace($"{invocation.TargetType.FullName}#{invocation.Method.Name} method start ");
            invocation.Proceed();
            logger.Trace($"{invocation.TargetType.FullName}#{invocation.Method.Name} method end ");
        }
    }
}

Bootstrapper

Bootstrapper では DIコンテナを設定する。
(Prism 7以降では DIコンテナの設定は App.xaml.cs に移動している)

using Autofac;
using Prism.Autofac;
using Prism.Modularity;
using System;
using System.Windows;
using VmInjectionSample1.Views;
using VmInjectionSample1.ComponentManagement;
using System.Reflection;
using Autofac.Extras.DynamicProxy;

namespace VmInjectionSample1
{
    public class Bootstrapper : AutofacBootstrapper
    {
        /// <summary>
        /// コンストラクタ
        /// </summary>
        public Bootstrapper()
        {
        }

        protected override void ConfigureContainerBuilder(ContainerBuilder builder)
        {
            base.ConfigureContainerBuilder(builder);
            // Shell の登録
            builder.RegisterType<MainWindow>();


            // 共有オブジェクトの依存関係(ログ、interceptorなど)
            builder.RegisterType<TraceInterceptor>();
            builder.RegisterAssemblyTypes(this.GetType().Assembly)
             .Where(t => t.Name.EndsWith("ViewModel"))
              .AsSelf()
              .EnableClassInterceptors()
              .InterceptedBy(typeof(TraceInterceptor))
              .InstancePerLifetimeScope()
              ;
        }

        protected override void InitializeShell()
        {
            var window = (Window)Shell;
            window.Show();
        }

        protected override DependencyObject CreateShell()
        {
            return Container.Resolve<MainWindow>();
        }

        protected override void ConfigureModuleCatalog()
        {
            // AutofacではPrism.Moduleを使えない
        }
    }
}

View

ここでは単純に数値をインクリメント、デクリメントするだけのViewを使う。

<UserControl x:Class="VmInjectionSample1.Views.SampleView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:VmInjectionSample1.Views"
             xmlns:prism="http://prismlibrary.com/"
             prism:ViewModelLocator.AutoWireViewModel="True"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800"
             FontSize="16">
    <Grid Margin="8">
        <StackPanel>
            <TextBox Text="{Binding NumberProperty.Value}" Margin="0 0 0 8"></TextBox>
            <DockPanel>
                <StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="0 0 8 0">
                    <Button Content="Incremenet" Command="{Binding IncrementCommand}"></Button>
                </StackPanel>
                <StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="0 0 8 0">
                    <Button Content="Decrement" Command="{Binding DecrementCommand}"></Button>
                </StackPanel>

            </DockPanel>
        </StackPanel>
    </Grid>
</UserControl>

ViewModel

SampleView のViewModel

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Prism.Mvvm;
using VmInjectionSample1.ComponentManagement;
using Reactive.Bindings;
namespace VmInjectionSample1.ViewModels
{
    public class SampleViewModel : BindableBase
    {
        public virtual ReactiveProperty<int> NumberProperty { get; set; } = new ReactiveProperty<int>();

        public ReactiveCommand IncrementCommand { get; set; } = new ReactiveCommand();
        public ReactiveCommand DecrementCommand { get; set; } = new ReactiveCommand();

        public SampleViewModel()
        {
            IncrementCommand.Subscribe(() => { Increment(); });
            DecrementCommand.Subscribe(() => { Decrement(); });
        }

        protected virtual void Increment()
        {
            NumberProperty.Value++;
        }
        protected virtual void Decrement()
        {
            NumberProperty.Value--;
        }
    }
}

ReactivePropertyとトレース対象メソッドは virtual にしないと Interceptor が認識できない。

MainWindow.xaml.cs

RegisterForNavigation だけだと最初のViewのロードがうまくできないらしいので、最初のViewへの移動を MainWindow.xaml.cs で行う。

using Prism.Regions;
using System.Windows;

namespace VmInjectionSample1.Views
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow(IRegionManager regionManager)
        {
            InitializeComponent();
            regionManager.RegisterViewWithRegion("ContentRegion", typeof(SampleView));
        }
    }
}

nlog 設定

まあ、いつも使う設定でよい。

  <nlog>
    <targets>
      <target name="file" type="File" layout="${longdate} [${threadid:padding=8}] [${uppercase:${level:padding=-5}}] ${message} ${exception:format=ShortType,Message,StackTrace,Data:maxInnerExceptionLevel=10:separator=\r\n}" fileName="${basedir}/logs/${shortdate}.log" encoding="UTF-8" archiveFileName="${basedir}/logs/archives/archive.{#}.log" archiveEvery="Day" archiveNumbering="Rolling" maxArchiveFiles="7" />
      <target name="null" type="Null" layout="${message}"/>
      <target name="console" type="Console" layout="${message}"/>
    </targets>
    <rules>
      <logger name="*" minlevel="Trace" writeTo="file,console" />
    </rules>
  </nlog>

実行

単純に実行してボタンを押してみる。

その他

Prism 7 以降だと Autofacが使えない。Unityでは Interceptor の作り方が違うので、同じ Interceptor を使いたい場合は DryIoc に移行することになる。

ここのサンプルではすべての ViewModel が Interceptor 組み込み対象になっているが、クラスやメソッドに属性を追加してIntercept対象かどうか判定したほうが良い。

プロパティの更新が反映されないように見える場合

このサンプルだとうまくいっているはずだが、 ReactiveProperty を使わないでバッキングフィールド付きのプロパティにした場合などにプロパティの更新がうまくいかないように見える場合がある。

この場合は

  • プロパティを virtual にする
  • プロパティを更新した後で RaisePropertyChanged(nameof(Foo)); を実行する

などしてみると良い。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?