воскресенье, 1 ноября 2009 г.

LINQ dynamic expressions. WHERE <T> IN <Filters>

В LINQ есть конструкция, которая позволяет делать SQL запрос с выражением IN. Все примеры я буду приводить на всем известной базе данных Northwind.

Например чтоб получить запрос на SQL в виде


SELECT c.* 
FROM Customers c
WHERE c.Region IN ("WA", "SP", "BC")

нужно написать на LINQ следующий код

string[] filters = new [] { "WA", "SP", "BC" };
context.Customers.Where(p => filters.Contains(p.Region));

Данная конструкция нечитабельная, лично меня она напрягает. Мне хотелось бы чтоб она выглядела более естественно, как то так:

string[] filters = new [] { "WA", "SP", "BC" };
context.Customers.Where(p => p.Region.In(filters));

Конечно так написать нельзя и данных код нескомпилируется, но сделать запрос более естественным можно. Нам помогут Expression Trees, примеры которых вы могли видеть в предыдущих статьях. Далее будет рассказано, как сделать код вида:

string[] filters = new [] { "WA", "SP", "BC" };
var customers = context.Customers.In(p => p.Region, filters);

Погуглив, я нашел пример в котором делается примерно тоже самое, только проще и с обманом. В моем случае, на выходе мы получим SQL запрос с конструкцией WHERE IN, а в примере получается … AND (c.Region = “WA” OR c.Region = “SP” …). Я думаю парень не выдержал, задача все-таки нелегкая. Я тоже понервничал пока разобрался как транслировать в конструкцию IN. Вам будет проще :)

Начнем.
Ниже приведен код “In” Extenstion method’a и подробное его описание по шагам.

public static class LinqExtensions
{
     /// <summary>
     /// Filters the specified query with input filters.
     /// </summary>
     /// <typeparam name="T">The result entity type.</typeparam>
     /// <typeparam name="TKey">The type of column to filter.</typeparam>
     /// <param name="query" />The query.</param>
     /// <param name="column" />The column.</param>
     /// <param name="filters" />The in sequence.</param>
     public static IQueryable<T> In<T, TKey>(
          this IQueryable<T> query, 
          Expression<Func<T, TKey>> column,
          IEnumerable<TKey> filters)
     {
          MethodInfo containsMethod = typeof(System.Linq.Enumerable)
               .GetMethods()
               .Where(m => m.Name == "Contains" && m.GetParameters().Length == 2)
               .Single();

          containsMethod = containsMethod.MakeGenericMethod(new[] { typeof(TKey) });

          List<expression> values = new List<expression>();
          foreach (TKey value in filters)
          {
               values.Add(Expression.Constant(value));
          }

          NewArrayExpression filtersArray = Expression.NewArrayInit(typeof(TKey), values);

          List<expression> arguments = new List<expression>();
          arguments.Add(filtersArray);
          arguments.Add(column.Body);

          MethodCallExpression containsMethodCall = Expression.Call(containsMethod, arguments.ToArray());

          LambdaExpression lambda = Expression.Lambda(containsMethodCall, column.Parameters.ToArray());

          var result = Expression.Call(
               typeof(Queryable),
               "Where",
               new Type[] { query.ElementType },
               query.Expression,
               lambda);

          return query.Provider.CreateQuery<T>(result);
     }
}

Рассмотрим подробней:

  • Сначала нам надо найти метод Contains (17-20). Поскольку этот метод является Extenstion Method, то его нужно искать в классе, в котором хранятся эти расширения. В данном случае System.Linq.Enumerable. Методов Contains два:

    Contains>TSource<(IEnumerable>TSource<, TSource)
    Contains>TSource<(IEnumerable>TSource<, TSource, IEqualityComparer>TSource<) 
    

    нам нужен [1] с двумя параметрами.
  • После того как мы его нашли, у нас будет нетипизированный метод в виде Contains[TSource], а нам нужен Contains, который работает с типом поля, которое нам передается. В данном примере мы фильтровали по Region у которого тип System.String. Делаем с общего метода, типизированный (23). В результате получаем сигнатуру вида Contains<String>(<IEnumerable<String>, String>).
  • Затем нам нужен список значений, по которым будет фильтроваться выборка. Для этого создаем набор констант (24-28) и помещаем их в массив (30). В результате мы получаем динамическое выражение вида new[] { “WA”, “BC”, “SP”}.
  • Согласно сигнатуре метода Contains [1] мы должны передать в него набор фильтров (33) и параметр-поле (34), по которым будет фильтроваться выборка. Делаем вызов метода Contains (36). В результате мы получаем динамическое выражение вида new[] { “WA”, “BC”, “SP”}.Contains(p.Region). Тут мы как раз делаем саму IN конструкцию.
  • После этого нам нужно создать лямбда выражение (38), которое будет передаваться в конструкцию Where. В результате мы получаем динамическое выражение вида p => new[] { “WA”, “BC”, “SP”}.Contains(p.Region). Входящий параметр лямбда выражения будет таким, как вы его назовете при использовании нашего “In” Exstension method. В данном примере он называется p, т.к. я делал вызов вида context.Customers.In(p => p.Region, filters).
  • Все. Нам осталось добавить в существующую цепочку вызовов наш метод Where (40-45).

Думаю вы почерпнулия для себя что-то новое и полезное.

пятница, 21 августа 2009 г.

Editable Text с использование IValueConverter

Сегодня я хотел бы рассказать об использование IValueConverter интерфейсе и как используют ValueConvertor’ы в WPF.

ValueConvertor’ы очень полезная и удобная штука, которая очень часто используется при биндинге (будем по народному)для преобразования одного типа данных в другой, например:
  • Вы делаете компонент который отображает прогресс какого-то действия и вам надо менять цвет от зеленого до красного, в зависимости от того, на сколько прогресс движется к концу;
  • Вам надо сделать какой-то расчет, например зарплату по количеству часов * рейт работника;
  • Или более простые пример, это отформатировать дату или преобразовать цвет по #AABBCCDD в строковое представление;
  • Инвертировать отображения элемента, например у вас биндится свойство на CheckBox типо IsDeclined и если оно true, то CheckBox не должен быть отмечен (пример не надуманый, всякое бывает). Если есть возможность, то можно переименовать в Allowed и будет логично, а если нет? Тогда делаем сами уже знаете что.

Конверторы инкапсулируют какую-то логику, которая может меняться, они также могут принимать параметры. Также конверторы используются для отладки биндинга (об этом позже).

Я думаю у многих возникала потребность в редактируемом тексте, например отображается текст в метке “Edit me”, вы щелкаете по тексту, он меняется на TextBox, вы его редактируете, нажимаете Enter и опять метка, круто.

Рассмотрим как это можно сделать с использованием конверторов. Определим правила игры:

  1. Метка может иметь начальный текст
  2. Метка переходит в состояние редактирования по щелчку мышки на ней
  3. TextBox переходит в состояние метки после нажатия клавиши Enter или когда пользователь уводит курсор с TextBox или теряется фокус.

Рассмотрим XAML нашего компонента:

<UserControl x:Class="EditableLabel.Control.EditableText"
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
     xmlns:Converters="clr-namespace:EditableLabel.Converters"    
     x:Name="editableText">
     <UserControl.Resources>
          <Converters:BooleanToVisibleConverter x:Key="converter" />
     </UserControl.Resources>    
     <Grid>
          <Label x:Name="label" 
               Content="{Binding ElementName=editableTextBox, Path=Text}" 
               MouseLeftButtonDown="OnMouseLeftButtonClick"
               Visibility="{Binding AllowEdit, ElementName=editableText,
               Converter={StaticResource converter}, ConverterParameter=True}"/>

          <TextBox x:Name="editableTextBox" 
               LostFocus="OnTextBoxLostFocus" 
               MouseLeave="OnTextBoxMouseLeave" 
               KeyDown="OnTextBoxKeyDown" 
               Text="{Binding Text, ElementName=editableText}"
               Visibility="{Binding AllowEdit, ElementName=editableText,
               Converter={StaticResource converter}, ConverterParameter=False}"/>
     </Grid>
</UserControl>

У нас есть Label и TextBox. Когда AllowEdit свойство True отображается TextBox, в другом случае Label.

Текст для TextBox берется из свойства Text компонента, а текст для метки берется из TextBox. Зависимость выглядит так:

    Label.Text < –  TextBox.Text < – > UserControl.Text

В ресурсах у нас объявлен конвертер, который используется для свойства Visibility у Label и TextBox.

Visibility="{Binding AllowEdit, ElementName=editableText,
Converter={StaticResource converter}, ConverterParameter=False}
вот его код:
namespace EditableLabel.Converters
{
     [ValueConversion(typeof(bool), typeof(Visibility))]
     public class BooleanToVisibleConverter : IValueConverter
     {
          public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
          {
               bool flag = (bool) value;
               bool inverted = false;

               if (parameter != null)
               {
                    inverted = Boolean.Parse(parameter.ToString());
               }

               return inverted ? 
                    (flag ? Visibility.Hidden : Visibility.Visible) 
                    : (flag ? Visibility.Visible : Visibility.Hidden);
          }

          public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
          {
               throw new NotImplementedException();
          }
     }
}

Метод ConvertBack очень редко используется, в некоторых случаях при двунаправленном биндинге (расскажу в другой раз), поэтому всегда остается таким и не вызывается в данном случае.

В метод Convert, в параметр value приходит значение, которое мы биндим, в данном случае AllowEdit, а в parameter будет передано значение, которое мы указали в ConverterParameter (если не указали - null соответственно).

ConverterParameter не единственный способ передать параметры в класс конвертора. Если вам надо передать несколько параметров это можно сделать так:

[ValueConversion(typeof(bool), typeof(Visibility))]
public class BooleanToVisibleConverter : IValueConverter
{
     public string AdditionalValue { get; set; }

     public int AnotherValue { get; set; }

     public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
}

и затем использовать в разметке или в коде так:

<Converters:BooleanToVisibleConverter 
     x:Key="converter" 
     AdditionalValue="asd" 
     AnotherValue="2" />

Единственная проблема, если у вас биндятся ListBoxItem’s, например, и у каждого свое значение для конвертора, то такой способ объявления конвертора со свойствами не прокатит. Этот способ хорошо когда конвертор используется 1 раз. В другом случае придется использовать IMultiValueConverter. Принцип тот же самый, только в Convert передается object[] values.

ValueConversion атрибут используется для того, чтоб быстрей было понять, из какого в какой тип происходит конвертация. Этот атрибут не обязателен, просто best practice + удобство чтения.

Код событий:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace EditableLabel.Control
{
    /// <summary>
    /// Interaction logic for EditableText.xaml
    /// </summary>
    public partial class EditableText : UserControl
    {
        public static readonly DependencyProperty TextProperty =
            DependencyProperty.Register("Text", typeof (string), typeof (EditableText));

        public static readonly DependencyProperty AllowEditProperty =
            DependencyProperty.Register("AllowEdit", typeof (bool), typeof (EditableText));

        public bool AllowEdit
        {
            get { return (bool) GetValue(AllowEditProperty); }
            set { SetValue(AllowEditProperty, value); }
        }

        public string Text
        {
            get { return (string) GetValue(TextProperty); }
            set { SetValue(TextProperty, value); }
        }

        public EditableText()
        {
            InitializeComponent();
        }

        private void OnMouseLeftButtonClick(object sender, MouseButtonEventArgs e)
        {
            AllowEdit = true;
            editableTextBox.Focus();
        }

        private void OnTextBoxLostFocus(object sender, RoutedEventArgs e)
        {
            AllowEdit = false;
        }

        private void OnTextBoxMouseLeave(object sender, MouseEventArgs e)
        {
            AllowEdit = false;
        }

        private void OnTextBoxKeyDown(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Enter)
            {
                AllowEdit = false;
            }
        }
    }
}

Варианты для усовершенствования:
  • Cделать чтоб ширна TextBox’a была такой же как у метки;
  • Возможно вынести в отдельную сборку для дальнейшего использования и сделать как Custom Control, может потом захочется вид изменить или эффекты прикрутить.

вторник, 28 июля 2009 г.

LINQ dynamic expressions. Where

Я как-то писал пример, как можно создать динамическое выражения для сортировки. Сегодня мы рассмотрим еще один пример, как можно написать динамическое выражения для фильтрации по конкретному полю.

Здесь выполняются те же действия, что и в OrderBy, только добавилось еще обращение к значению фильтра 'value'. Пример упрощен тем, что для фильтрации предполагается использование оператора '='.

public static class WhereExtension
{
     /// <summary>Filters input sequence with specified value.</summary>
     /// <param name="query">The query.</param>
     /// <param name="column">The column name to filter.</param>
     /// <param name="value">The value to filter with.</param>
     public static IQueryable<T> Where<T>(this IQueryable<T> query, string column, object value)
     {
          if (string.IsNullOrEmpty(column))
          {
               return query;
          }

          // Where(p => p.{Column} == {value})
          PropertyInfo property = query.ElementType.GetProperty(column);
          ParameterExpression parameter = Expression.Parameter(query.ElementType, "p");
          MemberExpression memberAccess = Expression.MakeMemberAccess(parameter, property);
          ConstantExpression filter = Expression.Constant(value);
          BinaryExpression condition = Expression.Equal(memberAccess, filter);
          LambdaExpression lambda = Expression.Lambda(condition, parameter);

          MethodCallExpression result = Expression.Call(
               typeof(Queryable), "Where",
               new [] { query.ElementType},
               query.Expression, 
               lambda);

          return query.Provider.CreateQuery<T>(result);
     }
}

Разобраться не сложно.

Возможности для усовершенствования:
  • Расширить операторы фильтрации, например '>', '<', '<>' и тд.

вторник, 28 апреля 2009 г.

LINQ dynamic expressions. OrderBy

В одном из проектов я писал клиентский компонент который отображал табличные данные (грид). Эти данные приходили из веб сервиса и передавались в грид.

Веб сервис выглядел так:

public List<Student> GetStudents()
{
     var query = (from student in db.Students
                  select new
                  {
                       student.StudentID,
                       student.LastName,
                       student.FirstName,
                       ...
                  }
     ...
}

Потом добавилось требования сортировать грид по нажатию на шапку колонки. Данные о сортируемой колонке должны были передаваться в сервис:
public List<Student> GetStudents(string column, bool asc)

и тут стал вопрос "а как ?", если у меня анонимный тип. Это потом я уже собираю в Student:
Вариант в лоб:

if (!asc)
{
     switch(column)
     {
          case "FirstName": 
         query = query.OrderByDescending(p => p.FirstName);
          break;
    
          case "LastName": 
         query = query.OrderByDescending(p => p.LastName);
          break;
          ...
          ... 
    // All columns
     }
}
else
{
     switch(column)
     {
          case "FirstName": 
         query = query.OrderBy(p => p.FirstName);
          break;

          case "LastName": 
         query = query.OrderBy(p => p.LastName);
          break;
          ...
          ... 
    // All columns
     }
}

Что, не? Та да... :)

Второй вариант и наверно единственный (если вы не мазохист) - это использовать LINQ Expressions. Что оно такое и как работает читайте в msdn или на блогах. Я напишу свое решение относительно моих требований и условий. В результате получится:

query = query.OrderBy(column, asc);

Для того чтоб все казалось как "так и надо" я использовал Extension Methods, которые появились в .NET 3.0.

public static class OrderByExtension
{
     /// <summary>Orders the sequence by specific column and direction.</summary>
     /// <param name="query">The query.</param>
     /// <param name="sortColumn">The sort column.</param>
     /// <param name="ascending">if set to true [ascending].</param>
     public static IQueryable<T> OrderBy<T>(this IQueryable<T> query, string sortColumn, string direction)
     {
          string methodName = string.Format("OrderBy{0}", direction.ToLower() == "ascending" ? "" : direction);

          // .OrderBy{Direction}(p => p.{ColumnName})
          PropertyInfo property = query.ElementType.GetProperty(sortColumn);
          ParameterExpression parameter = Expression.Parameter(query.ElementType, "p");
          MemberExpression paramAccess = Expression.MakeMemberAccess(parameter, property);
          LambdaExpression orderByLambda = Expression.Lambda(paramAccess, parameter);

          MethodCallExpression result = Expression.Call(
                    typeof(Queryable),
                    methodName,
                    new[] { query.ElementType, property.PropertyType },
                    query.Expression,
                    Expression.Quote(orderByLambda));

          return query.Provider.CreateQuery<T>(result);
     }
}

В общем виде формирование запроса разбивается на следующие этапы:
  1. Определяем метод сортировки OrderBy или OrderByDescending.
  2. Получить ссылку на поле, которое участвует в результирующей выборке.
  3. Далее формируем lambda выражение: p => p.[ColumnName], где в [ColumnName] будет подставлено поле, которое мы получили в пункте 2.
  4. Создаем вызов метода OrderBy например, который использует labda из пункта 3.: OrderBy(p => p.[ColumnName])
  5. Внедряем наш метод в цепочку вызовов: return query.Provider.CreateQuery(result)
Пример использования выше. Получилось гламурно :). Если будут вопросы пишите.

ReportViewer не работает под IIS 7

Недавно пришлось использовать компонент ReportViewer в ASP.NET.
У меня на ноутбуке стоит Vista Business и IIS7.
При открытии страницы в IE, FireFox - получаю кучу JS ошибок. Проблема кроется в новой структуре Web.config файла для IIS 7.
Когда вы добавляете на страницу компонент ReportViewer, студия добавляет ссылку на http handler для этого компонента. Это нужно для того, чтоб ASP.NET использовало сборку Microsoft.Reporting.WebForms.HttpHandler при обращении к ReportViewerWebControl.axd:

<system.web>
<httpHandlers>
<add path="Reserved.ReportViewerWebControl.axd"
verb="*"
type="Microsoft.Reporting.WebForms.HttpHandler, Microsoft.ReportViewer.WebForms, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
validate="false"/>
<httpHandlers>
<system.web>



В новой структуре секция system.web\httpHandlers теперь перенесена в system.webServer\handlers, но студия редактирует конфиг как для IIS 6.

Для решения проблемы надо создать соответствующую секцию:

<system.webServer>
<handlers>
   <add name="ReportViewerWebControl"
           path="Reserved.ReportViewerWebControl.axd" 
verb="*"
           type="Microsoft.Reporting.WebForms.HttpHandler, Microsoft.ReportViewer.WebForms, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"/>
   <handlers>
<system.webServer>


сross post: http://blogs.msdn.com/vijaysk/archive/2009/01/14/report-viewer-toolbar-does-not-render-properly-on-iis-7-0.aspx