לדלג לתוכן

ממשק משתמש ורכיבים מעורבים

חלון שמפסיק לצייר בזמן חישוב אינו בהכרח תקלה בגרפיקה. אם ה־thread של ה־dispatcher עסוק בלולאה שלנו, הוא אינו פנוי לעבד הודעות, לבצע layout ולהריץ handlers. נבנה חלון WPF קטן שמפריד בין עבודה חישובית לבין שינוי מצב התצוגה, ואז נחבר את ההפרדה הזאת ל־COM ולרכיבים טבעיים.

צרו פרויקט console חדש והחליפו את קובץ הפרויקט ואת Program.cs בתוכן הבא. הפרויקט מיועד לווינדוס עם .NET 10 ו־WindowsDesktop runtime, ואינו דורש XAML כדי להדגים את המנגנון.

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net10.0-windows</TargetFramework>
    <UseWPF>true</UseWPF>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

static class Program
{
    [STAThread]
    public static void Main() => new Application().Run(new MainWindow());
}
sealed class ViewState : INotifyPropertyChanged
{
    private string text = "Ready";
    public string Text
    {
        get => text;
        set { text = value; PropertyChanged?.Invoke(this, new(nameof(Text))); }
    }
    public event PropertyChangedEventHandler? PropertyChanged;
}
sealed class MainWindow : Window
{
    private readonly ViewState state = new();
    private readonly Button start = new() { Content = "Calculate" };
    private CancellationTokenSource? stop;
    public MainWindow()
    {
        Title = "Managed work";
        Width = 380;
        Height = 170;
        DataContext = state;
        var cancel = new Button { Content = "Cancel" };
        var label = new TextBlock();
        label.SetBinding(TextBlock.TextProperty, new Binding(nameof(ViewState.Text)));
        var panel = new StackPanel { Margin = new Thickness(16) };
        panel.Children.Add(start);
        panel.Children.Add(cancel);
        panel.Children.Add(label);
        Content = panel;
        start.Click += Calculate;
        cancel.Click += (_, _) => stop?.Cancel();
        Closed += (_, _) => stop?.Cancel();
    }
    private async void Calculate(object sender, RoutedEventArgs args)
    {
        start.IsEnabled = false;
        using var cancellation = new CancellationTokenSource();
        stop = cancellation;
        state.Text = "Working";
        try
        {
            ulong result = await Task.Run(() => Sum(cancellation.Token));
            state.Text = result.ToString();
        }
        catch (OperationCanceledException) { state.Text = "Canceled"; }
        catch (Exception error) { state.Text = error.Message; }
        finally { stop = null; start.IsEnabled = true; }
    }
    private static ulong Sum(CancellationToken token)
    {
        ulong total = 0;
        for (int i = 0; i < 20_000_000; i++)
        {
            if ((i & 4095) == 0) token.ThrowIfCancellationRequested();
            total += (uint)i;
        }
        return total;
    }
}

ה־Task.Run מתאים כאן מפני שהעבודה חישובית. ה־await ב־handler מחזיר את המשך הפעולה להקשר ה־UI, ולכן שינוי state והכפתור מתבצע על ה־dispatcher. ה־binding מקבל PropertyChanged ומעדכן את התצוגה. אם נזיז את ההשמה ל־state.Text לתוך worker, ניצור חוזה עמום לגבי ה־thread שממנו מופיעות ההתראות; נרצה לחזור במפורש ל־dispatcher עבור מצב שמשמש את הממשק.

הכפתור מושבת בזמן פעולה כדי שלא נחליף את מקור הביטול באמצע ריצה. הסגירה מבקשת ביטול, אבל הדוגמה אינה מציגה פרוטוקול join מלא לפני יציאת התהליך. היא אינה מחזיקה native buffer או מבצעת commit, ולכן התנהגות כזאת מוגדרת כגבול המעבדה. מוצר שיש לו פעולות שצריכות להסתיים יבטל את Closing הזמני, יבקש shutdown, ימתין לו ואז יסגור פעם אחת לפי מכונת מצבים מפורשת.

ה־STAThread ב־Main מתחבר גם לעולם COM. אובייקט COM יכול להיות קשור ל־apartment, והעברת reference ל־thread אחר אינה בהכרח הרשאה לקרוא לו שם. ה־RCW מייצג אובייקט COM בצד המנוהל, וה־CCW מציג אובייקט מנוהל לצרכן COM. המעטפות מנהלות קשר בין references, זהות ומנגנוני קריאה, אבל אינם מבטלים את חוזי apartment ואת הצורך ב־message pumping כאשר הממשק מחייב זאת.

קריאה ל־Marshal.FinalReleaseComObject על wrapper משותף יכולה לנתק משאב שצרכן אחר עדיין צריך. לכן לא נוסיף אותה כפתרון אוטומטי לכל עלייה במספר אובייקטים. נבדוק את הבעלות לפי הספרייה, ונבחין בין ממשק שדורש close מפורש לבין שחרור wrapper. בקוד חדש קיימים גם כלים ל־COM interop שנוצר בזמן הבנייה, עם מגבלות וחוזים שונים מן המודל הוותיק.

ה־C++/CLI מאפשר גשר עשיר בין C++ לקוד מנוהל על Windows, אבל מוסיף תלות ב־toolchain ובמודל הפצה מסוים. ה־IPC נותן גבול תהליך שמאפשר בידוד כשלים ומודל חיות נפרד, במחיר serialization והעברת הודעות. נבחר לפי הצורך ב־ABI, ביצועים, אמון והפצה. התיעוד על מודל התהליכונים של WPF ועל COM interop משלים את המודל שלמדנו בפרקי Win32 ו־COM.