Category Archives: WindowsForms

Code Review: Tworzenie wątków za pomocą Task.Factory.StartNew i ContinueWith

Załóżmy, że nie możemy korzystać z await\async i gdzieś w kodzie mamy następujące wywołania:

public MainWindow()
{
  InitializeComponent();
  Task.Factory.StartNew(LoadData).ContinueWith(UpdateUserInterface, TaskScheduler.FromCurrentSynchronizationContext());
}
private static void UpdateUserInterface(Task obj)
{
  Console.WriteLine("Interfejs zaktualizowany.");
  Task task = Task.Factory.StartNew(AnotherTimeConsumingOperation);
}

private static void AnotherTimeConsumingOperation()
{
  // ???
}

private static void LoadData()
{
  Thread.Sleep(5000);
}

Chcemy wykonać operacje za pomocą Task’ow. LoadData to operacja czasochłonna i wykonujemy ją w wątku z puli. Następnie chcemy zaktualizować interfejs użytkownika więc wywołujemy UpdateUserInterface na wątku głównym UI. Na końcu chcemy wykonać kolejną operację więc standardowo wywołujemy StartNew:

Task.Factory.StartNew(AnotherTimeConsumingOperation);

Niestety, jeśli zajrzyjmy do debuggera, okaże się, że ta metoda jest wykonywana na wątku UI!

image

Dlaczego? W końcu spodziewalibyśmy się, że zostanie ściągnięty wątek z puli, ponieważ nie użyliśmy żadnej flagi mówiącej o synchronizacji (tak jak w poprzednim przykładzie).

Zajrzyjmy do Reflector’a:

[MethodImpl(MethodImplOptions.NoInlining), __DynamicallyInvokable]
public unsafe Task StartNew(Action action)
{
    StackCrawlMark mark;
    Task task;
    mark = 1;
    task = Task.InternalCurrent;
    return Task.InternalStartNew(task, action, null, this.m_defaultCancellationToken, this.GetDefaultScheduler(task), this.m_defaultCreationOptions, 0, &mark);
}

 

Widać, że jest używana metoda GetDefaultScheduler do uzyskania scheduler’a:

private TaskScheduler GetDefaultScheduler(Task currTask)
{
    if (this.m_defaultScheduler == null)
    {
        goto Label_000F;
    }
    return this.m_defaultScheduler;
Label_000F:
    if (currTask == null)
    {
        goto Label_0024;
    }
    if ((currTask.CreationOptions & 0x10) != null)
    {
        goto Label_0024;
    }
    return currTask.ExecutingTaskScheduler;
Label_0024:
    return TaskScheduler.Default;
}
  private TaskScheduler GetDefaultScheduler(Task currTask)
{
    if (this.m_defaultScheduler == null)
    {
        goto Label_000F;
    }
    return this.m_defaultScheduler;
Label_000F:
    if (currTask == null)
    {
        goto Label_0024;
    }
    if ((currTask.CreationOptions & 0x10) != null)
    {
        goto Label_0024;
    }
    return currTask.ExecutingTaskScheduler;
Label_0024:
    return TaskScheduler.Default;
}

 

Łatwo sprawdzić, że defaultscheduler będzie NULL więc pierwszy IF nie zostanie wykonany.

Okazuje się, że w naszym przypadku zostanie użyty aktualny scheduler, którym po wywołaniu UpdateUserInterface będzie SynchronizationContextScheduler. Zatem wszystkie wątki od czasu gdzie przekazaliśmy jako scheduler TaskScheduler.FromCurrentSynchronizationContext() będą wykonywane domyślnie na wątku UI.

Rozwiązanie to oczywiście przekazywanie zawsze schedulera jawnie:

Task.Factory.StartNew(AnotherTimeConsumingOperation,new CancellationToken(),TaskCreationOptions.None,TaskScheduler.Default)

Innymi słowy, jeśli nie przekażemy schedulera jawnie to jest używany TaskScheduler.Current a nie TaskScheduler.Default. Z tego względu, zawsze należy jawnie przekazywać taki parametr ponieważ łatwo popełnić błąd. Myślę, że nie jest to dla większości programistów zbyt intuicyjne. Szczególnie, gdy nie mamy jednego łańcucha wywołań a tworzymy wątki w różnym miejscach.

ASP .NET Membership w WinForms.

ASP .NET Membership jest bardzo dobrym rozwiązaniem dla uwierzytelnienia oraz autoryzacji użytkowników w aplikacji webowej. Umożliwia m.in. zarządzanie kontami użytkowników czy tworzenie ról. Często jednak system składa się również z aplikacji typu desktop. W rozbudowanych systemach proponuję zrezygnować z czystego ASP .NET Membership na rzecz Windows Identity Framework. Jeśli jednak tworzymy małą aplikacje z pewnością przyda nam się wsparcie ASP .NET Membership w WinForms.

Rozwiązaniem problemu są tzw. client application services. Zacznijmy jednak od samego początku:

  1. Najpierw tworzymy oczywiście aplikację web ASP .NET.
  2. Oprócz autoryzacji istnieje również możliwość udostępniania właściwości aplikacji WinForms. Dodajemy przykładową właściwość:
    <profile>
      <properties>
          <add name="Color" type="string" defaultValue="Blue"/>
        </properties>
    </profile>
    ...
    </system.web>

  3. W celu udostępnienia właściwości Color wystarczy w w web.config dodać następujący wpis:
    <system.web.extensions>
        <scripting>
            <webServices>
                <authenticationService enabled="true" />
                <roleService enabled="true"/>
                <profileService enabled="true" readAccessProperties="Color" writeAccessProperties="Color"/>
            </webServices>        
        </scripting>
    </system.web.extensions>

Powyższy kod również udostępnia usługę uwierzytelnienia oraz zarządzania rolami.

Następnym etapem jest konfiguracja aplikacji WinForms:

  1. Tworzymy aplikacje typu WindowsForms.
  2. Wchodzimy we właściwości projektu a konkretnie w zakładkę Services. Następnie ustawiamy prawidłowy adres. Jeśli zakłada nie jest aktywna to prawdopodobnie  został ustawiony .NET Framework Client Profile i należy ustawić np. NET Framework 4.0.

    image 

  3. Wchodzimy w zakładkę Settings oraz klikamy w przycisk Load Web Settings. Po wpisaniu loginu i hasła zostaną zwrócone udostępnione właściwości:

image 

Od tej pory w kodzie aplikacji WinForms możemy odwoływać się do właściwości jak do zwykłych zasobów, np:

public Form1()
{
  InitializeComponent();
  if(Login())
    this.BackColor = Color.FromName(Properties.Settings.Default.Color) ;
}
private bool Login()
{
  bool result = System.Web.Security.Membership.ValidateUser("admin", "password");
  if (result)
      MessageBox.Show("Zalogowano.", "Autoryzacja", MessageBoxButtons.OK,MessageBoxIcon.Information);
  else
      MessageBox.Show("Nieprawidłowy login lub hasło.", "Autoryzacja", MessageBoxButtons.OK,MessageBoxIcon.Error);

  return result;
}

Podobnie można zapisywać właściwość:

Properties.Settings.Default.Color = Color.Red.ToString();
Properties.Settings.Default.Save();

W celu przekonania się, że to naprawdę działa ustawmy kolor również w aplikacji web:

protected void Page_Load(object sender, EventArgs e)
{                                        
  Button1.BackColor = System.Drawing.Color.FromName(Context.Profile.GetPropertyValue("Color").ToString());
}

Oczywiście aby kod zadziałał użytkownik musi być zalogowany (np. za pomocą gotowej kontrolki Login).

W podobny sposób można używać metody InRole (sprawdzanie czy użytkownik należy do danej grupy). Warto podkreślić, że za pomocą client application service można korzystać z Membership również w aplikacjach WPF, AJAX czy nawet Silverlight.

Aktualizacja interfejsu z drugiego wątku(windows forms i WPF)

Na różnych forach często użytkownicy mają problem z aktualizacją kontrolek z innego wątku. Załóżmy, że odpaliliśmy sobie BackgroundWorker lub po prostu Thread. Wykonujemy jakieś operację, np. łączymy się ze zdalnymi zasobami. Stworzenie osobnego wątku w takim scenariuszu jest bardzo pożądane ponieważ nie blokujemy wtedy aktualizacji interfejsu. W trakcie pobierania informacji z Internetu chcemy aktualizować interfejs aby informować użytkownika o postępach np.

progressBar.Value = progessValue;

Jeśli powyższy kod jest wywołany z obcego wątku użytkownik dostanie następujący komunikat o błędzie:

Cross-thread operation not valid:
Control accessed from a thread other than the thread it was created on.

Wyjątek jest wyrzucany ponieważ zarówno w WindowsForms jak i w WPF nie można aktualizować interfejsu z innego wątku niż z tego w którym została stworzona kontrolka. Musimy więc w jakiś sposób dostać się do wątku macierzystego dla kontrolki i tam wykonać aktualizacje tej kontrolki. Służy do tego metoda Control.Invoke(windows forms) lub ControlDispatcher.Invoke(Wpf). Chcąc więc zaktualizować ProgressBar w WinForms musimy napisać:

Action<int> updateAction = new Action<int>((value) => progressBar.Value = value);
progressBar.Invoke(updateAction,32);

Powyższy kod będzie działał w każdym przypadku ale ma jedną wadę związaną z wydajnością kodu. Co w przypadku gdy mamy osobną klasę do aktualizacji interfejsu i jest ona wywoływana zarówno z wątku kontrolki jak i z obcego wątku? Dla drugiego przypadku(z obcego wątku) kod jest maksymalnie optymalny. Z kolei w sytuacji gdy wywołujemy ją z wątku macierzystego dla kontrolki nie potrzebnie będzie wykonywana masa operacji związanych z wpompowaniem operacji w kolejkę dla wątku interfejsu. Na szczęście istnieje gotowa metoda, która stwierdza czy dla aktualnego przypadku użycia jest wymagane wywołanie Invoke:

Action<int> updateAction = new Action<int>((value) => progressBar.Value = value);
if (progressBar.InvokeRequired)
    progressBar.Invoke(updateAction,5);
else
    updateAction(4);

Posiadamy już optymalną wersje kodu. Jednak pisanie powyższego kodu za każdym razem gdy chcemy zaktualizować interfejs(nigdy nie wiemy czy kod nie będzie wywoływany w przyszłości z innego wątku) jest co najmniej niewygodne. Z ratunkiem przychodzą nam tzw. rozszerzenia(extensions,c# 3.0). Możemy przecież stworzyć własną metodę nazwaną powiedzmy InvokeIfRequired:

public static class ControlExtensions
{
   public static void InvokeIfRequired(this Control control, Action action)
   {
       if (control.InvokeRequired)
           control.Invoke(action);
       else
           action();
   }
   public static void InvokeIfRequired<T>(this Control control, Action<T> action, T parameter)
   {
       if (control.InvokeRequired)
           control.Invoke(action, parameter);
       else
           action(parameter);
   }
}

Za pomocą takiego rozwiązania aktualizacja kontrolki sprowadzi się wyłącznie do poniższego kodu:

this.InvokeIfRequired((value) => progressBar.Value = value, 10);

W przypadku Wpf rozwiązanie jest bardzo podobne:

public static class ControlExtensions
{
   public static void InvokeIfRequired(this Control control, Action action)
   {
       if (System.Threading.Thread.CurrentThread!=control.Dispatcher.Thread)
           control.Dispatcher.Invoke(action);
       else
           action();
   }
   public static void InvokeIfRequired<T>(this Control control, Action<T> action, T parameter)
   {
       if (System.Threading.Thread.CurrentThread!=control.Dispatcher.Thread)
           control.Dispatcher.Invoke(action, parameter);
       else
           action(parameter);
   }
}

Z kolei użycie metody jest identyczne jak w przypadku WinForms:

this.InvokeIfRequired((value) => bar.Value = value, 10);