вторник, 17 сентября 2013 г.

Слияние двух выражений (Linq.Expression)

Постановка задачи

Работая с технологией Linq, я недавно уперся в следующую проблему. В рамках моей задачи, мне было необходимо выражение вида Expression<Func<T1,bool>>, которое я должен был передать в Linq-запрос. При этом, извне мне передали выражение вида Expression<Func<T2,bool>>. Кроме того, у меня есть преобразование вида Expression<Func<T1,T2>>. Если бы мы имели дело с простыми делегатами типа Func, то все решилось бы последовательным вызовом двух функций: f = x => f1( f2( x ) );. Однако, с выражениями, мы такой финт сделать не можем, а приведения выражений к делегатам методом Compile(), создадут нам анонимные классы и последующая свертка сведется, соответственно, к вызову методов этих классов. А это значит, что результирующее выражение уже можно будет использовать только, как делегат - полноценного дерева выражений (к примеру, для построения SQL запроса) уже не будет.

Решение

Что бы сохранить нормальную структуру наших выражений, будем использовать класс ExpressionVisitor, с помощью которого обойдем дерево первого выражения и вместо переменной типа T2 подставим выражение для T1=>T2. Для этого напишем класс-наследник ExpressionVisitor:
public class ReplacementVisitor : ExpressionVisitor
{
   readonly Expression fromExpr;
   readonly Expression toExpr;

   public ReplacementVisitor( Expression oldExpr, Expression newExpr )
   {
      fromExpr = oldExpr;
      toExpr = newExpr;
   }

   public override Expression Visit( Expression node )
   {
      if( node == fromExpr )
         return toExpr;
      return base.Visit( node );
   }
}
Используя этот класс, создадим статическую функцию, которая собственно и будет решать нашу задачу
public static class ExpressionHelper
{
   public static Expression<Func<TSrc, TRes>> Combine<TSrc, TMes, TRes>( this Expression<Func<TSrc, TMes>> firstExpr, Expression<Func<TMes, TRes>> secondExpr )
   {
      var param = Expression.Parameter( typeof( TSrc ), "x" );
      var visitor = new ReplacementVisitor( firstExpr.Parameters[0], param );
      var newParentBody = visitor.Visit( firstExpr.Body );
      visitor = new ReplacementVisitor( secondExpr.Parameters[0], newParentBody );
      var body = visitor.Visit( secondExpr.Body );
      return Expression.Lambda<Func<TSrc, TRes>>( body, param );
   }
}
Осталось только сказать, что класс ExpressionVisitor появился в .Net Framework 4.0. Если же вы используете более ранние версии .Net, то данный класс можно взять с сайта Microsoft. Например, отсюда http://msdn.microsoft.com/ru-ru/library/bb882521(v=vs.90).aspx.

воскресенье, 15 сентября 2013 г.

Использование Ninject в Windows Forms

На днях столкнулся с такой проблемой - как использовать Ninject совместно с дизайнером Windows Forms. Понятное дело, создать саму форму проблем нет. Для этого достаточно заменить строку new Form1() на kernel.Get<Form1>(). В частности, для главного окна программы, мы просто корректируем класс Program, заменяя строку Application.Run( new Form1() ); на:
var kernel = new StandardKernel( new MyInjectModule() );
Application.Run( kernel.Get<Form1>() );
Но что же делать с пользовательскими контролями и компонентами, размещенными в дизайнере? Ведь код для их создания генерируется автоматически. Тут нас выручит возможность произвести инъекцию в уже готовый экземпляр с помощью метода Inject, определенного в интерфейсе IKernel и специальный атрибут InjectAttribute, позволяющий делать инъекции в свойства и методы. Итак, предположим, мы создали наше окно через Ninject, указанным выше способом. В конструкторе нашего окна вызывается сгенерированный студией метод InitializeComponent, который отрабатывает код дизайнера, рекурсивно создавая контроли и компоненты, расположенные в окне. Далее, наша задача рекурсивно обойти все эти элементы, вызывая для каждого метод Inject. В общем-то, ничего сложного в этом нет. А, что бы было еще проще, я написал маленький класс-хелпер. Выглядит он следующим образом:
public static class NinjectHelper
{
 public static void InjectToComponent( this IKernel kernel, Component parent )
 {
  if( parent is Control )
   foreach( Control c in ((Control)parent).Controls )
   {
    kernel.Inject( c );
    kernel.InjectToComponent( c );
   }
  var cmpInfo = parent.GetType().GetField( "components", BindingFlags.NonPublic | BindingFlags.Instance );
  if( cmpInfo != null )
  {
   var cmp = cmpInfo.GetValue( parent ) as IContainer;
   if( cmp != null )
    foreach( Component c in cmp.Components )
    {
     kernel.Inject( c );
     kernel.InjectToComponent( c );
    }
  }
 }
}
Использовать его очень просто - для этого надо вызвать метод InjectToComponent сразу после метода InitializeComponent:
public Form1( IKernel kernel )
{
 InitializeComponent();
 kernel.InjectToComponent( this );
}
После этого мы можем получить инъекции в любом нашем подэлементе (контроле или компоненте) через атрибут InjectAttribute следующим образом:
[Inject]
public int A
{
 get { return a; }
 set
 {
  a = value;
  //do some things
 }
}
Или, аналогично, через метод:
[Inject]
public void SetA( int a )
{
 //do some things
}
P. S. Я уверен, что найдутся люди, которые скажут "Вай! Зачем такие сложности? Не проще положить kernel в статический класс и дергать его оттуда из любого места?". Не смотря, на очевидность ответа на этот вопрос, я все же его озвучу. Если вы пишете универсальные блоки кода, предполагая их использование из разных приложений, то вы не можете знать, где и как создается ядро kernel - ваш статический класс вполне может оказаться непроинициализированным. Если же вы пишете небольшое приложение, не предполагающее особой гибкости, то Ninject вам вообще не нужен - все можно получить с помощью статических хелперов.