воскресенье, 19 февраля 2012 г.

Отменяемая асинхронная операция при помощи Task Parallel Library .NET 4

Недавно в одной WinForms программе мне понадобилось воспользоваться асинхронными операции, чтобы не блокировать графический интерфейс во время длительных расчетов. Это можно реализовать, конечно же, очень многими способами, от “ручного” программирования потоков (Threads) до использования таких конструкций как BackgroundWorker. Но мне захотелось (наконец-то!) попробовать новые библиотеки для параллельного программирования появившиеся в .NET 4 версии.

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

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


    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Linq;
    using System.Text;
    using System.Windows.Forms;
    using System.Threading;
    using System.Threading.Tasks;

    namespace WindowsFormsApplication1
    {
        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
            }

            private void startButton_Click(object sender, EventArgs e)
            {
                StopCalcIfRun();
                BeginCalc();
            }

            private void stopButton_Click(object sender, EventArgs e)
            {
                StopCalcIfRun();
            }

            // --------------------------------------

            class CalcResult
            {
                public int IntResult;
            }

            // объект задачи одновременно является флагом, нулевое значение которого указывает на то, запущена ли операция
            volatile Task calcTask = null;

            void StopCalcIfRun()
            {
                Task task = calcTask;

                if (task != null)
                {
                    // извлекаем CancellationTokenSource для отмены операции
                    CancellationTokenSource ts = (CancellationTokenSource)task.AsyncState;
                    ts.Cancel();

                    try
                    {
                        task.Wait();
                    }
                    catch (AggregateException e)
                    {
                        // Если e.InnerExceptions содержит исключение TaskCanceledException,
                        // то это указывает на "успешную" отмену задачи, а не на сбой
                    }
                }
            }

            void BeginCalc()
            {
                TaskScheduler uiTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();

                CancellationTokenSource tockenSource = new CancellationTokenSource();
                CancellationToken tocken = tockenSource.Token;

                // Создаем и запускаем длительную операцию.
                // Вниманине, tockenSource "закрепляется" за задачей как State Object, и используется для ее отмены в StopCalcIfRun методе
                calcTask = Task.Factory.StartNew((xxx) =>
                {
                    try
                    {
                        // уже отменили задачу?
                        tocken.ThrowIfCancellationRequested(); // *** Отмена задачи ***
                        CalcResult result = new CalcResult();

                        // длительные вычисления
                        result.IntResult++;
                        System.Threading.Thread.Sleep(1000);
                     
                        if (tocken.IsCancellationRequested) { // *** Отмена задачи ***
                            // ... освобождаем ресурсы
                            tocken.ThrowIfCancellationRequested();
                        }

                        // продолжаем длительные вычисления
                        result.IntResult++;
                        System.Threading.Thread.Sleep(1000);
                     
                        if (tocken.IsCancellationRequested) { // *** Отмена задачи ***
                            // ... освобождаем ресурсы
                            tocken.ThrowIfCancellationRequested();
                        }
                     
                        return result;
                    }
                    finally
                    {
                        calcTask = null;
                    }
                }, tockenSource, tocken);

                // Это продолжение будет вызвано в случае возникновения исключения (см. TaskContinuationOptions).
                // Исключение TaskCanceledException отменяющее задачу является корректным завершением задачи
                // и к таким исключениям не относится!
                calcTask.ContinueWith((ant) =>
                {
                     MessageBox.Show(ant.Exception.InnerException.ToString());
                }
                    , System.Threading.CancellationToken.None
                    , TaskContinuationOptions.OnlyOnFaulted
                    , uiTaskScheduler);

                // Продолжение если задача отменяется.
                // ant.Result есть null, не стоит к нему обращатся
                calcTask.ContinueWith((ant) =>
                {
                    resultTextBox.Text += "Canceled;";
                }
                    , System.Threading.CancellationToken.None
                    , TaskContinuationOptions.OnlyOnCanceled
                    , uiTaskScheduler);

                // Полное завершение задачи => отображаем результаты
                calcTask.ContinueWith((ant) =>
                {
                    resultTextBox.Text += ant.Result.IntResult + ";";
                }
                    , System.Threading.CancellationToken.None
                    , TaskContinuationOptions.OnlyOnRanToCompletion
                    , uiTaskScheduler);
            }
        }
    }


Ресурсы:

Threading in C#, Joseph Albahari
http://www.albahari.com/threading/part5.aspx

Перевод части статьи Джозефа Албахари (Joseph Albahari) от Sergey Teplyakov
http://sergeyteplyakov.blogspot.com/2010/09/52.html

Все дело в SynchronizationContext [RU. MSDN Magazine]
http://msdn.microsoft.com/ru-ru/magazine/gg598924.aspx

Отмена задач [RU. MSDN Статья]
http://msdn.microsoft.com/ru-ru/library/dd997396.aspx

суббота, 28 января 2012 г.

Visual Studio 2010 Color Schema. Моя темная цветовая схема

Моя цветовая схема, которой я пользуюсь уже достаточно продолжительное время.

Редакотор c# кода:


XAML:


XML:


Основной каркас этой схемы когда-то накопал на сайте studiostyl.es, как она называлась изначально уже и не вспомню. Постепенно, по мере надобности, оптимизировал ее до существующей. Цвета, на мой взгляд, достаточно мягкие, что бы смотреть на них долгое время, глаза не устают.

Скачать схему

понедельник, 23 января 2012 г.

Visual Studio. Поиск русских букв в коде

Иногда, когда я пишу прототипы или набрасываю код, то снабжаю его на скорую русскими комментариями. В конечном продукте заметок на русском быть не должно, и я, прорабатывая результат, стараюсь от них избавится. Что бы не пропустить ни одного использую стандартный поиск (Ctrl+F) с regex выражением “[а-яА-ЯёЁ]”.

воскресенье, 22 января 2012 г.

MS SQL. Выборка данных без учета диакритических знаков

Встала задача находить записи в базе данных, которые содержали диакритические знаки без указания этих знаков. Например, если представить, что в базе находится фирма “Télécöm”, хотелось бы находить ее по значению “Telecom”. В результаты наших запросов эти значения не попадали.

Проблема таилась в сортировке, которая использовалась в базе данных по умолчанию: Latin1_General_ci_AS. Эта сортировка с суффиксом “_AS” различает символы с диакритическими знаками, так что 'a' и 'ấ' это не одно и то же. Сортировка с суффиксом “_AI”, ей в противоположность, считает эти символы идентичными. Подробнее о сортировках можно почитать здесь.

Итак, проблему решили путем применения сортировки Latin1_General_ci_AI на выбираемые колонки при помощи оператора COLLATE:

SELECT ID, Company
FROM Companies
WHERE (Company LIKE N'Telecom' COLLATE Latin1_General_ci_AI)

В заключении хочу добавить, что сортировка вообще определяется MS SQL Server-ом на нескольких уровнях: от уровня сервера, через уровень базы, затем через уровень столбца, до уровня выражений. Мы применили уровень выражения, но можно было бы, конечно, использовать и другие уровни.

воскресенье, 18 декабря 2011 г.

DisplayText для ComboBox элементов в WinForms

Многие стандартные контролы в WinForms при отображении списков элементов объектов (контролы типа ComboBox), используют возвращаемое этими объектами значение метода ToString(). Вообще, использование метода ToString() в этих целях является стандартным подходом и используется также в других библиотеках WinForms, например в DevExpress контролах.

Интересно, сколько различных способов существует для придания спискам объектов нужного представления. Кто-то наследуется от класса, который нужно отобразить, и переопределяет его метод ToString(). Иногда, что на мой вгляд хуже всего, пытаются в классах модели определить их представление и уже в них самих переопределяют ToString(). Кто-то использует словари (Dictionary) для сопоставления отображаемого текста с объектами. Можно использовать Binding.

Есть еще парочка известных мне методов, но я предпочитаю варант предсталенный ниже. Это некоторая общая обертка (Generic Wrapper), которая содержит всего два свойства: объект и его желаемое отображаемое значение:

class DisplayItem {
     public DisplayItem(T value, String dispText) {
         Value = value;
         DisplayText = dispText;
     }

     public T Value { get; set; }
     public String DisplayText { get; set; }

     public override string ToString() {
         return DisplayText ;
     }
}

среда, 11 мая 2011 г.

Пустое содержимое в FolderBrowserDialog

Во время создания пользовательского окна в проекте установки (Visual Studio, Setup-Project) у меня возникли проблемы при отображении содержимого окна FolderBrowserDialog:

Быстрый поиск по инету указал на то, что пользоваться этим окно можно лишь в STA-потоке. Обычно этот параметер задается в начальной точке приложения в атрибуте STAThread:
[STAThread] static void Main() { ... }.

Но как указать модель STA в приложении, в котором не владеешь стартовой точкой, в таком как Setup-проект? Использовал следующий трюк, где я вызываю FolderBrowserDialog в новом потоке, для которого задаю STA-модель перед его старом. Сработало.

// Обработчик события кнопки выбора пути
private void dbPathButton_Click(object sender, EventArgs e) {

// Нормальный подход, но не работает
// FolderBrowserDialog fbd = new FolderBrowserDialog();
// fbd.SelectedPath = dbPathTextBox.Text;
// if (fbd.ShowDialog(this) == System.Windows.Forms.DialogResult.OK)
// dbPathTextBox.Text = fbd.SelectedPath;

// Пришлось делать так:
pathBuff = dbPathTextBox.Text;
Thread thr = new Thread(ShowFolderBrowserDialog);
thr.SetApartmentState(ApartmentState.STA);
thr.Start();
thr.Join();
dbPathTextBox.Text = pathBuff;
}

String pathBuff = String.Empty;

public void ShowFolderBrowserDialog() {
FolderBrowserDialog fbd = new FolderBrowserDialog();
fbd.SelectedPath = pathBuff;
if (fbd.ShowDialog() == System.Windows.Forms.DialogResult.OK)
pathBuff = fbd.SelectedPath;
}

среда, 2 февраля 2011 г.

ToString Or Empty If Null. Строковое значение объекта. Маленькие хитрости

Недавно мне понадобилось получать строковые значения некоторых объектов, т.е. нужно было получать результат вызова метода ToString(). Если объект нулевой (null) нужно было получать пустую строку. Задача осложнялась тем, что таких объектов было много. А в некоторых случаях нужно было получить строковое значение членов объектов (свойств), которые, в свою очередь, также могли быть нулевыми.

Но буду последователен и посмотрим, как бы решалась задача для простого типа, например String:
String obj = "value";
// ..
String str = (obj == null) ? String.Empty : obj.ToString();


* This source code was highlighted with Source Code Highlighter.

Как уже было сказано, проверок на null оказалось слишком много, так что код начал терять читабельность. Пришлось вводить метод-расширение. За счет этого удалось сократить код и повысить его восприятие:
String obj = "value";
// ..
String str = obj.xStringOrEmptyIfNull();


* This source code was highlighted with Source Code Highlighter.

Метод-расширение выглядит так:
public static class ExtensionsString
{
  public static String xStringOrEmptyIfNull(this Object obj) {
    return (obj != null) ? obj.ToString() : String.Empty;
  }
}


* This source code was highlighted with Source Code Highlighter.

Все очень просто, но не забываем про вторую часть задачи, где нужно получать строковое значение свойства объекта. Вот классы для примера:
public class Person
{
  public String Name { get; set; }
  public Language Language { get; set; }
}

public class Language
{
  public String LangName { get; set; }
  public int Level { get; set; }
}


* This source code was highlighted with Source Code Highlighter.

Имея объект класса Person нужно определить название языка, либо получить пустую строку, если свойство язык равно нулю. Решение в лоб:
String lang = (p.Language != null) ? p.Language.LangName.xStringOrEmptyIfNull() : String.Empty;

* This source code was highlighted with Source Code Highlighter.

Выглядит не очень, и это несмотря на использование метода-расширения, существенно сокращающего код. Так вот, мне удалось сократить код до слудующей конструкции:
String lang = p.Language.xStringOrEmptyIfNull(x => x.LangName);

* This source code was highlighted with Source Code Highlighter.

Уже намного лучше. Вот так выглядит метод-расширение:
public static String xStringOrEmptyIfNull<T>(this T t, Func<T, Object> del) {
  return (t == null) ? String.Empty : del(t).xStringOrEmptyIfNull();
}


* This source code was highlighted with Source Code Highlighter.

Весь секрет в делегатае Func<..>. Кто еще не очень с ним знаком, советую на него более пристально посмотреть. Вся магия LINQ построена на нем и я сам иногда прибегаю к его использованию, например, так как это сделано в примере выше.