ThreadingCzęść NR.5 Jak można wykorzystać programowanie aspektowe w pożyteczny sposób?

Interceptory mogą się przydać przy tworzeniu nowych wątków. Pisząc aplikację w WPF lub Windows Forms łatwo zauważyć tę sytuację. Podczas wykonywania jakiejś długiej czynności w kodzie możesz zauważyć, że cały wątek UI został zatrzymany. Oznacza to, że nie możesz ruszyć okna, nie możesz skrolować okna i nie możesz na nic klikać, bo cały wątek odpowiedzialny za to jest zajęty.

Jakie jest więc rozwiązanie? Proste, wykonać czynność, która pochłania czas w innym wątku.

gif2[5] 

Sprawa oczywiście nie jest taka prosta. Po wykonaniu tej długiej czynności w innym wątku zazwyczaj chcemy wyświetlić wynik procesu w jakiejś kontrolce. Aby to zrobić musimy wrócić do wątku UI.

Jeśli tego nie zrobimy otrzymamy wyjątek InvalidOperationException.

image

Jak się domyślasz wywołanie nowego wątku oraz powrót do wątku UI wymaga pewnego kodu. Kodu, który będzie się powtarzał.

Ten powtarzający się kod można wydzielić do aspektu. Co właśnie pokażę na przykładzie WPF i Windows Forms

Przykład Windows Forms

Oto prosta aplikacja, która posiada jeden przycisk i listę wiadomości, które będę obierał

image

A oto nasz serwis, który zwróci mi komunikat po dwóch sekundach.

public class SomeService
{
    public string GetMessage()
    {
        Thread.Sleep(2000); 
        return "Message from " + DateTime.Now.TimeOfDay;
    }
}

Przejdę jednak do najważniejszej części aplikacji. W tym przykładzie AOP skorzystam z PostSharpa. Powód jest prosty intercepcja za pomocą Castle.Windsor wymagałaby całej architektury, a chodzi o prosty przykład, a nie o budowę gigantycznej aplikacji.

Mamy więc pierwszy aspekt – pierwszy interceptor, który wykona cały kod metody (args.Proceed) w innym wątku.

[Serializable]
public class AnotherThread : MethodInterceptionAspect
{
    public override void OnInvoke(MethodInterceptionArgs args)
    {
        var thread = new Thread(args.Proceed);
        thread.Start();
        //var task = new Task(args.Proceed);
        //task.Start();
    }
}

Do szczęścia potrzebny nam jest jeszcze drugi aspekt, który zaktualizuje nam potrzebne dane w wątku UI. W tym aspekcie sprawdzam, czy rzeczywiście jest poza wątkiem UI, jeśli tak jest, to do niego wracam.

[Serializable]
public class UIThread : MethodInterceptionAspect
{
    public override void OnInvoke(MethodInterceptionArgs args)
    {
        var form = (Form) args.Instance;
        if (form.InvokeRequired)
            form.Invoke(new Action(args.Proceed));
        else
            args.Proceed();
    }
}

[cloud-green:InvokeRequired]Właściwość tylko do odczytu InvokeRequired pozwala mi zbadać, czy obecnie jestem w wątku UI lub nie. Jeśli w nim nie jestem, muszę w takim razie wykonać kod UI w wątku UI przy pomocy metody Invoke, w Windows Forms.

Jak się przekonasz podobny wzorzec występuje także w WPF.[gc]

Mając gotowe aspekty pozostaje ich już tylko użyć. W aplikacji Windows Form tworzę instancje klasy SomeService.

public partial class Form1 : Form
{
    SomeService _service;
    
    public Form1()
    {
        InitializeComponent();
    }
    
    protected override void OnLoad(EventArgs e)
    {
        _service = new SomeService();
    }
    
    private void btnUpdate_Click(object sender, EventArgs e)
    {
        GetNewMessage();
    }

Cała magia jednak jest tutaj. Chcę, aby wysłanie wiadomości wykonało się w innym wątku, więc do metody dodaję atrybut aspektu “AnotherThread”.

Aktualizację kontrolki chcę wykonać w wątku UI, więc otaczam metodę aspektem “UIThread”.

[AnotherThread]
void GetNewMessage()
{
    var message = _service.GetMessage();
    UpdateListBox(message);
}

[UIThread]
void UpdateListBox(string msg)
{
    listMessages.Items.Add(msg);
}

Po kompilacji aplikacji i kompilacji PostSharp. Magia programowania aspektowego sprawia, że teraz mam aplikację, która się nie wiesza pod wypływem naciśnięcia przycisku.

GIF

Przejdź do przykładu WPF.

Przykład z WPF

Ponownie mam aplikację, która ma listę i przycisk.

image

Oto kod XAML.

<Window x:Class="WPFThreadingPostSharp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WPFThreadingPostSharp"
        mc:Ignorable="d"
        Title="MainWindow" Height="240" Width="278"   ResizeMode="NoResize">
    <Grid >
        <Button x:Name="Btn_" Content="Update" HorizontalAlignment="Left" VerticalAlignment="Top" Width="262" Click="Btn_Click" Height="40"/>
        <ListBox x:Name="listmsg" HorizontalAlignment="Left" Height="169" Margin="0,40,0,0" VerticalAlignment="Top" Width="262"/>

    </Grid>
</Window>

Analogicznie do poprzedniego przykładu i tym razem tworzę aspekty. Kod jednak będzie się trochę różnił. Zwłaszcza jeśli spojrzysz na aspekt wątku UI.

Tak jak wcześniej, sprawdzam, w jakim wątku obecnie się znajduję. Jeśli nie jestem w wątku UI, to wykonuje metodę interceptowaną w wątku UI.

 

[Serializable]
public class UIThread : MethodInterceptionAspect
{
    public override void OnInvoke(MethodInterceptionArgs args)
    {
        if (!Application.Current.Dispatcher.CheckAccess()) 
            // CheckAccess returns true if you're on the dispatcher thread
        {
            Application.Current.Dispatcher.BeginInvoke(
                DispatcherPriority.Normal, new Action(() =>
                {
                   args.Proceed();
                }));
        }
        else
        {
            args.Proceed();
        }

    }
}


Aspekt innego wątku wygląda tak samo.

[Serializable]
public class AnotherThread : MethodInterceptionAspect
{
    public override void OnInvoke(MethodInterceptionArgs args)
    {
        var thread = new Thread(args.Proceed);
        thread.Start();
        //var task = new Task(args.Proceed);
        //task.Start();
    }
}

Pod kodem głównego okna znajduje się też podobny kod.

public partial class MainWindow : Window
{

    SomeService _service;
    public MainWindow()
    {
        InitializeComponent();
        _service = new SomeService();
    }

    private void Btn_Click(object sender, RoutedEventArgs e)
    {
        GetNewMessage();
    }

    [AnotherThread]
    void GetNewMessage()
    {
        var msg = _service.GetMessage();
        UpdateListBox(msg);
    }

    [UIThread]
    void UpdateListBox(string msg)
    {
        listmsg.Items.Add(msg);
    }
}

To wszystko. Oto prosty pokaz tego, jak nagle skomplikowany problem może wydać się teraz bardzo przejrzysty i łatwy do zarządzania, dzięki interceptorom z PostSharpa.