среда, 2 декабря 2009 г.

SQL Поиск по шаблону. PATINDEX

Недавно коллега с соседней команды подкинули мне интересную задачу: у вас алфавитное меню A-Z, при клике на букву в подменю должны отобразится записи, которые начинаются на выбранную букву. Заполнением подменю занимается аяксовый сервис, который дергает хранимку, которая должна вернуть данные наичнающиеся с указанной буквы(такая была архитектура). Меню строится для продуктов. Ниже пример названий продуктов.

Как видите не все так сладко. Некоторые продукты не начинаются с буквы. Допустим нам нужны продукты которые начинаются на 'А'. Должны отобразится 128, 130-135, 137, 138, 140 и тд. Я привел только те, которые видны на картинке. В базе 300 000 продуктов, а подменю должно отображатся как подменю :)
Как решить задачу ?

Решение 1:
- Добавить колонку в таблицу, например [FirstLetter]. Вызвать один раз хранимку или серверный код, не важно, который заполнит это поле. Потому можно проиндексивать поле и выгребать данные как угодно и быстро. Или если товары импортируются, делать это при импорте.

Решение 2:
- Если "Решение 1" не подходит или нет возможности его реализовать, есть другой вариант. Можно выбрать один раз все имена продуктов на сервере, закешировать, поставить SqlCacheDependency если можно и нужно. Потом в сервисе можно в сохраненной выборке перебирать данные с помощью Regex, LINQ, искать первую букву, как угодно, возможностей много. Данное решение хорошее, но держать в кешэ 300 000 записей жирно та и требует изменений в существующей архитектуре - хочется сделать минимальные усилия с максимальным результатом исходя из того, что есть.

Решение 3:
- Хотелось бы воспользоваться оператором LIKE в котором можно задать примитивный шаблон типо регулярки, но как я не крутил с LIKE, у меня ничего не получилось, но я был на правильном пути. И тогда я вспомнил что есть встроенная функция PATINDEX, которая делает тоже самое, что и LIKE, только еще и возвращает индекс совпадения этого шаблона. По скольку нам нужно найти в названии первую букву и сравнить её с заданой, то это можно написать так (например для буквы 'A'):

SELECT [Name] AS 'Product Name'
FROM Product
WHERE SUBSTRING(LOWER([Name]), PATINDEX('%[a-z]%', LOWER([Name])), 1) = 'a'

В результате получим:


Данные выбираются меньше секунды. Задача решена как и хотелось, с минимальными усилиями и максимальным успехом.
А теперь как это работает: функция PATINDEX ищет первое совпадение по указанному шаблону (с синтаксисом шаблона можно ознакомится по ссылкам). Наш шаблон говорит "искать первую попавщуюся букву в вырежении". Нашли, получили индекс этой буквы. Вырезаем эту букву функцией SUBSTRING и сравниваем с заданной.
Успехов.

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

LINQ Bulk Insert

Недавно я столкнулся с проблемой произовдительности вставки данных с помощью L2S. Представьте себе следующую ситуацию: у нас e-commerce сайт, в котором можно создавать продукты, категории и мапить продукты на категорию (ии). Остановимся на функционале, где можно мапить продукты на категорию. У вас есть форма для редактирования категории, на которой в таблице отображаются продукты. Таблица пейжирована. Также есть 2 чекбокса - check\ unckeck all и check\unckeck current page ну и кнопка Save.




Вы отмечаете check all нажимаете Save и жопа ждете пока сохраниться мап всех товаров, которые у вас есть на категорию. Не важно большой у вас ассортимент или нет, это будет выполнятся долго, т.к. будет дергаться INSERT INTO CategoryToProductMap столько раз, сколько у вас товаров. В моем случаей это было 10 000 раз. Потом я встретил еще чудо-код, который выполнял подобные действия 130 000 раз ! Вы представляете это !? А если хотя бы 2 человека попадут на эту страницу...

В данной статье мы поговорим о том как можно оптимзировать процесс вставки большого количества данных, большого значит > 1 000. Хотя вы сможете применять это по своим нуждам.

В MS SQL 2000 появилась команда BULK INSERT, которая возволяет загрузить данные из файла в SQL Server "пачкой". Внутри эта команда использует утилиту BCP, копирует данные из файла в указанную таблицу. В ADO.NET 2.0 появился класс SqlBulkCopy, который является обверткой над командой BULK INSERT. В его использовании нет ничего сложного. Вы указываете строку соединения, таблицу в которую хотите вставить данные и сами данные.
Это нам подходит! Надо бы сделать это гламурненько в стиле L2S.
Погуглив, я нашел пример как это делается в стиле L2S, но как по мне он сложноват как в исполнении так и в чтении. Если хотите, можете взять его, он рабочий, я проверил. Но! У меня получилось быстрей :). Почему - не знаю. В конце статьи я приведу примеры по скорости выполнения.
Для реализации своего примера я почерпнул пару штрихов из источника, поэтому желательно если вы с ним ознакомитесь, хотя и необязательно. Примеры я буду приводить на всем известной Northwind базе данных. В результате у нас получится слудующее:

IEnumerable<Customer> dataToInsert = GenerateCustomers(100000);
int batchSize = 5000;
context.Customers.BulkInsert(dataToInsert, batchSize);

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


мы будем использовать метод, который принимает IDataReader. У интерфейса IDataReader очень много методов, но нам необходимо только 4. Создадим класс реализующий этот интерфейс. Методы, которые нам нужны пометим как  abstract, остальные как virtual, они нам не нужны и я их не буду показывать в коде.

public abstract class SqlBulkCopyReader : IDataReader
{
     public abstract bool Read();
     public abstract object GetValue(int i);
     public abstract int GetOrdinal(string name);
     public abstract int FieldCount { get; }

     #region // Not required ...
     ....
     #endregion 
}

Отнаследуем класс от SqlBulkCopyReader:

public class LinqEntityDataReader<T> : SqlBulkCopyReader where T : class
{
     private DataTable _sourceTable;
     private readonly IEnumeratort<T> _enumerator;
     private DataRow _current;

     public LinqEntityDataReader(IEnumerable<T> source)
     {
           MapTableName();
           MapColumns();
           _enumerator = source.GetEnumerator();
     }

     public string DestenationTable 
     {
          get { return _sourceTable.TableName; }
     } 

     public IEnumerable<string> Columns
     {
          get
          {
               foreach (DataColumn column in _sourceTable.Columns)
               {
                    yield return column.ColumnName;
               }
           }
      }

      public override object GetValue(int columnIndex)
      {
           return _current[columnIndex];
      }

      public override int GetOrdinal(string name)
      {
           return _sourceTable.Columns[name].Ordinal;
      }

      public override bool Read()
      {
           bool next = _enumerator.MoveNext();

           if (next)
           {
                _current = _sourceTable.NewRow();

                foreach (DataColumn column in _sourceTable.Columns)
                {
                     _current[column] = typeof(T).GetProperty(column.ColumnName)
                           .GetValue(_enumerator.Current, null);
                }
           }

           return next;
     }

     public override int FieldCount
     {
          get { return _sourceTable.Columns.Count; }
     } 

     private void MapTableName()
     {
          Type entityType = typeof(T);

          TableAttribute destenationTable = entityType
              .GetCustomAttributes(typeof(TableAttribute), false)
              .Cast<TableAttribute>()
              .SingleOrDefault();

          if (destenationTable == null)
          {
               throw new ArgumentNullException("destenationTable");
          }

          _sourceTable = new DataTable(destenationTable.Name);
     }

     private void MapColumns()
     {
          Type entityType = typeof(T);
          int columnIndex = 0;

          foreach (PropertyInfo property in entityType.GetProperties())
          {
               ColumnAttribute column = property
                   .GetCustomAttributes(typeof(ColumnAttribute), false)
                   .Cast<columnattribute>()
                   .SingleOrDefault();
          }

          if (column == null)
          {
               return;
          }

          if (!column.IsVersion && !column.DbType.Contains("IDENTITY") && !column.IsDbGenerated)
          {
               _sourceTable.Columns.Add(column.Name ?? property.Name);
          }

          columnIndex++;
     }
}

Начнем с конструктора. Для начала мы должны знать куда нам вставлять данные. Это мы можем узнать у сущности из аттрибута Table, что и происходит в методе MapTableName. Затем нам нужно знать в какие колонки можно вставлять данные. Во вставке могу участвовать лубые колонки кроме IDENTITY, TIMESTAMP и тп. Это можно узнать у свойства сущности, которое помечено аттрибутом Column, что и происходит в методе MapColumns.
SqlBulkCopy начинает свою работу с получения количества колонок, которые участвую во вставке и если указан мапинг, то вызывается метод GetOrdinal, чтоб получить индекс каждой колонки по имени.
Затем SqlBulkCopy идет построчно вызывая метод Read, а потом вызывает GetValue для каждой колонки у текущей записи.
Мое "ноу-хау" - это получение значений колонок рефлексией, когда запрашивается очередная строка, и сохранение этих значений в DataRow. Во-первых это проще, во-вторых читабельней и понятней, а в-третьих - думаете рефлексия в методе Read будет все тормозить ? - Нет! Получилось только быстрей, на удивление (может кто-то скажет почему ?).

Ну и завершающий этап, сама обвертка над SqlBulkCopy в виде Extenstion Method для таблицы-сущности:

public static class TableExtension
{
 public static void BulkInsert<T>(this Table<T> entity, IEnumerable<T> data, int batchSize) 
  where T : class
 {
  LinqEntityDataReader<T> reader = new LinqEntityDataReader<T>(data);

  using (SqlBulkCopy sqlBulkCopy = new SqlBulkCopy(DBContext.ConnectionString))
  {
   foreach (string columnName in reader.Columns)
   {
    sqlBulkCopy.ColumnMappings.Add(columnName, columnName);
   }

   sqlBulkCopy.BatchSize = batchSize;
   sqlBulkCopy.DestinationTableName = reader.DestenationTable;
   sqlBulkCopy.WriteToServer(new LinqEntityDataReader<T>(data));
   sqlBulkCopy.Close();
  }
 }
}

Ну тут вроде ничего военного, все должно быть ясно. Ну и в результате:

IEnumerable<Customer> dataToInsert = GenerateCustomers(100000);
int batchSize = 5000;
context.Customers.BulkInsert(dataToInsert, batchSize);

Ну и как я обещал грубый тест на производительность. Тест производился на вставке 100 000 записей. BatchSize я подобрал оптимальную для данного количества записей. Забыл сказать, что от этого значения зависит много, вы можете как выиграть в производительности, так и не очень выиграть. Для того чтобы подобрать оптимальное значение надо поэксперементировать. Рекомендации мелкософта не помогли, все зависит от ситуации. Для создания фейковых данных использовался метод:

public static IEnumerable<Customer> GenerateCustomers(int count)
{
 for (int i = 0; i < count; i++)
 {
  yield return new Customer
              {
           CustomerID = i.ToString("00000"), 
   CompanyName = "Company" + i, 
   ContactName = "Frederique Citeaux",
                        ContactTitle = "Marketing Manager",
   Address = "24, place Kleber",
   Phone = "(604) 555-4729",
   Region = "WA"
         };
 }
}

Вот что у меня получилось:

  • Мое решение ~ 8 секунд
  • Решение из пример ~ 11секунд
  • Решение из пример + динамический вызов GetValue, который ему посоветовали в коментах ~ 9.5 секунд
  • Стандартный InsertAllOnSubmit ~ 1 минута 23 секунды
Я не думаю, что это совпадение, все-таки 100 000 данных - это немало. Почему так, я еще не узнал.
В любой случае я получил, что хотел. В моем конкретном случае мапинг всех товаров на категорию (10 000 записей) теперь выполняется ~ 1.1 секунды. Думаю этот пример вас когда-то спасет.

суббота, 7 ноября 2009 г.

LINQ dynamic expressions. WHERE <T> IN <Filters>. Part2 - Improvements

В предыдущей статье мы рассмотрели как можно создать аналог SQL опператора WHERE IN.
В результате мы получили LINQ выражение в вид:

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

В читабильности мы не достигли идеального результат, но Restuta предложил решение, как можно его улучшить.

Благодарности:
     Спасибо Restuta за предложенное улучшение.

Рассмотри что изменилось.

Для начала, нам необходимо создать вспомогательный класс, который, как сказал Restuta, надо "пропихнуть"... Наш класс будет выглядеть так:

public class QueryableProperty<T, TKey>
{
     /// <summary>Gets or sets the original query.</summary>
     /// <value>The query.</value>
     public IQueryable<T> Query { get; set; }

     /// <summary>Gets or sets the column to filter.</summary>
     /// <value>The column.</value>
     public Expression<Func<T, TKey>> Column { get; set; }
}

Затем нам необходим вспомогательный Extenstion Method, который как раз и будет "пропихивать" поле, которое мы хотим фильтровать и оригинальный запрос. Этот Extenstion Method как раз и является тем самым улучшением, который придает желаемой читабельности.

/// <summary>Aggregates property for future WHERE IN clause.</summary>
/// <typeparam name="T">The result entity type.</typeparam>
/// <typeparam name="TKey">The type of column to filter.</typeparam>
/// <param name="query" />The original query.</param>
/// <param name="column" />The column to filter.</param>
public static QueryableProperty<T, TKey> WhereProperty<T, TKey>(this IQueryable<T> query, Expression<Func<T, TKey>> column)
{
     return new QueryableProperty<T, TKey> { Query = query, Column = column };
}

Extenstion Method 'IN' почти не изменился, я не буду приводить его код, покажу только сигнатуру, по которой все станет ясно.

public static IQueryable<T> In<T, TKey>(this QueryableProperty<T, TKey> queryableProperty, IEnumerable<TKey> filters)

В результате, наше вырежание приняло следующий вид:

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

Получилось гламурненько. Как вы видите, цепочка вызовов не прерывается. Это самое главное. После In мы опять возвращаем IQueryable<T> и можем продолжать использовать другие Extension Methods.
Мы, конечно, можем обратиться к оригинальному запросу и после WhereProperty через свойство Query, но WhereProperty предназначался не для того.

Мы добились желаемого результат \m/

воскресенье, 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

среда, 25 марта 2009 г.

Улучшение производительности web сайтов. Web Garden

Всем привет.

Сегодня я начинаю свой блог и первую статью я посвящу улучшению производительности web сайтов.

Что я хочу рассказать:

Что такое Web Garden


Многие из вас сталкивались с проблемой, когда вашему web приложению трудно справляться с большим количеством подключений. Есть несколько способов решить эту трагедию:
  1. Проапгредить вашу старушку до «Мустанга» - дорого;
  2. Распараллелить web приложение – бесплатно и заманчиво;
  3. Распараллелить чтоб аж Google «завидовал» - в следующем посте (Web Farm).

Остановимся на 2 решении. Данный механизм называется Web Garden и служит для распараллеливания web приложений используя 1 компьютер. Это решение очень простое и не требует дополнительных ресурсов и растрат.

Веб садом является web приложение в котором количество рабочих процессов больше 1. Этот механизм появился с IIS 6, в котором также появилась возможность создавать N пулов под N сайтов (max 2000 pools). Каждый пул в IIS 6 по умолчанию работает в режиме isolation mode, это значит, что каждый пул будет иметь свою память, свой контекст и свои границы приложения. Также это значит, что если сайт «упадет» (упаси Билл конечно), то упадет только этот пул, с сайтами которые в нем существуют, но не весь сервер, как было раньше в IIS 5 %). По скольку пул может работать изолированно, мы можем дать ему возможность создавать еще процессы для того, чтоб приложение могло выдерживать больше подключений. Тем самым мы его распараллеливаем, снижая блокировки на ресурсы, используемые приложением. Все запросы, которые приходят в конкретный пул, разделяются между процессами web сада.


Как настроить Web Garden на IIS6

Рассмотрим как это делается.
  1. В IIS Manager создадим новый пул приложения, в котором будет работать наш сайт, например MyGardenPool.
  2. Затем выбираем в свойствах пула выбираем Properties.
  3. Переходим на вкладку Performance.
  4. В группе Web garden, в поле Maximum number of worker processes устанавливаем количество процессов которое будет выделено вашему пулу. Чтобы приложение работало как web сад, значение должно быть больше 1.
  5. Добавляем web приложение.

Для того чтобы оптимально подобрать количество процессов, поставьте для начала значение, равное количеству ядер или процессоров на вашей машине + 1. А дальше смотрите, если в вашем приложении используются долгие блокирующие запросы к БД, то вам лучше увеличить число выделяющихся процессов. Если запросы короткие и\или не блокирующие, то увеличивать число процессов не надо.


Преимущества
  1. Легко настраивается;
  2. Надежная и устойчивая обработка web сервером запросов. Если один из процессов в пуле будет работать не стабильно или «упадет» по неизвестным причинам, то его сразу подменить другой процесс из пула.
  3. Меньше блокировок на ресурсы. Когда web garden доходит до состоянии «щас лопну», то каждое новое подключение к серверу распределяется между процессами по карусельному принципу.
  4. Рациональное использование N-ядерной или N-процессорной машины;

Недостатки
  1. Если вы используете сессию для хранения каких то данные, то эти данные не будут переданы новому выделенному процессу, т.к. каждый новый процесс - это сам по себе работающий сайт со своей памятью, потоками и т.д. Эту проблему можно решить, например хранить сессию в SQL Server или State Server.
  2. Если у вас выделяется много процессов, то вы можете проиграть в скорости, т.к. время будет уходить на работу деспетчера процессов ОС.
  3. Выделяя больше процессов, вы выделяете под него память которую он «съедает», тем самым уменьшая память под другие возможности приложения, такие как кэш.

Вывод


Данный механизм очень хорошо себя оправдывает если вам надо быстро, просто и сердито улучшить производительность и выдерживаемую нагрузку на ваше приложение. Выигрыш в скорости зависит от количества процессоров на вашей машине. Так же не следует забывать о проблемах с сессией и не злоупотреблять количеством выделяемых процессов.