在本教學課程中,您會透過累加修改顯示隨機色彩清單的工具視窗,瞭解進階遠端 UI 概念:
您將了解:
- 如何讓多個異步命令同時執行,以及在命令執行時如何停用使用者介面元素。
- 如何將多個按鈕系結至相同的 異步命令。
- 如何在遠端 UI 資料上下文及其 Proxy 中處理參考型別。
- 如何使用 異步命令 作為事件處理程式。
- 如果多個按鈕系結至相同的命令,如何在 異步命令的回呼執行時停用單一按鈕。
- 如何從遠端 UI 控制件使用 XAML 資源字典。
- 如何在遠端 UI 的資料上下文中使用 WPF 類型,例如複雜的畫筆。
- 遠端 UI 如何處理線程。
本教學課程是以 遠端 UI 簡介文章為基礎,並預期您有一個運作中的 VisualStudio.Extensibility 延伸模組,包括:
-
.cs命令開啟工具視窗的檔案, -
MyToolWindow.cs類別的ToolWindow檔案 -
MyToolWindowContent.cs類別的RemoteUserControl檔案 -
MyToolWindowContent.xaml用於RemoteUserControlxaml 定義的嵌入資源文件 -
MyToolWindowData.cs檔案,用於RemoteUserControl的數據內容。
若要開始,請更新 MyToolWindowContent.xaml 以顯示清單檢視和按鈕」:
<DataTemplate xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vs="http://schemas.microsoft.com/visualstudio/extensibility/2022/xaml"
xmlns:styles="clr-namespace:Microsoft.VisualStudio.Shell;assembly=Microsoft.VisualStudio.Shell.15.0"
xmlns:colors="clr-namespace:Microsoft.VisualStudio.PlatformUI;assembly=Microsoft.VisualStudio.Shell.15.0">
<Grid x:Name="RootGrid">
<Grid.Resources>
<Style TargetType="ListView" BasedOn="{StaticResource {x:Static styles:VsResourceKeys.ThemedDialogListViewStyleKey}}" />
<Style TargetType="Button" BasedOn="{StaticResource {x:Static styles:VsResourceKeys.ButtonStyleKey}}" />
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource {x:Static styles:VsBrushes.WindowTextKey}}" />
</Style>
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListView ItemsSource="{Binding Colors}" HorizontalContentAlignment="Stretch">
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding ColorText}" />
<Rectangle Fill="{Binding Color}" Width="50px" Grid.Column="1" />
<Button Content="Remove" Grid.Column="2" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Button Content="Add color" Command="{Binding AddColorCommand}" Grid.Row="1" />
</Grid>
</DataTemplate>
然後,更新資料內容類別 MyToolWindowData.cs:
using Microsoft.VisualStudio.Extensibility.UI;
using System.Collections.ObjectModel;
using System.Runtime.Serialization;
using System.Text;
using System.Windows.Media;
namespace MyToolWindowExtension;
[DataContract]
internal class MyToolWindowData
{
private Random random = new();
public MyToolWindowData()
{
AddColorCommand = new AsyncCommand(async (parameter, cancellationToken) =>
{
await Task.Delay(TimeSpan.FromSeconds(2));
var color = new byte[3];
random.NextBytes(color);
Colors.Add(new MyColor(color[0], color[1], color[2]));
});
}
[DataMember]
public ObservableList<MyColor> Colors { get; } = new();
[DataMember]
public AsyncCommand AddColorCommand { get; }
[DataContract]
public class MyColor
{
public MyColor(byte r, byte g, byte b)
{
ColorText = Color = $"#{r:X2}{g:X2}{b:X2}";
}
[DataMember]
public string ColorText { get; }
[DataMember]
public string Color { get; }
}
}
此程式代碼中只有一些值得注意的事項:
-
MyColor.Colorstring通常是Brush,但當它在 XAML 中進行數據綁定時,它是 WPF 提供的一項功能。 - 異步
AddColorCommand回呼包含 2 秒的延遲,以模擬長時間執行的作業。 - 我們使用 ObservableList<T>,這是遠端 UI 提供的擴充 ObservableCollection<T>,以支援範圍操作,提供更佳的效能。
-
MyToolWindowData和MyColor不會實作 INotifyPropertyChanged, 因為目前所有屬性都是只讀的。
處理長時間運行的異步命令
遠端UI與一般 WPF 之間最重要的差異之一,就是牽涉到UI與延伸模組之間通訊的所有作業都是異步的。
異步命令,例如AddColorCommand透過提供異步回呼來明確執行此動作。
如果您在短時間內多次按兩下 [ 新增色彩 ] 按鈕,就會看到這個效果:因為每個命令執行需要 2 秒的時間,多個執行會平行發生,而且當 2 秒延遲結束時,多個色彩會出現在清單中。 這可能會讓使用者看到 [ 新增色彩 ] 按鈕無法運作。
若要解決此問題,請在 異步命令 執行時停用按鈕。 若要這麼做,最簡單的方式是將命令設定 CanExecute 為 false:
AddColorCommand = new AsyncCommand(async (parameter, ancellationToken) =>
{
AddColorCommand!.CanExecute = false;
try
{
await Task.Delay(TimeSpan.FromSeconds(2));
var color = new byte[3];
random.NextBytes(color);
Colors.Add(new MyColor(color[0], color[1], color[2]));
}
finally
{
AddColorCommand.CanExecute = true;
}
});
這個解決方案仍然沒有完美的同步處理,因為當使用者按鍵時,命令回呼會在延伸模組中以異步方式執行,回呼會設定 CanExecute 為 false,然後以異步方式傳播至 Visual Studio 進程中的 Proxy 數據內容,導致按鈕停用。 用戶可以在按鈕停用之前,快速連續點擊按鈕兩次。
更好的解決方案是使用RunningCommandsCount異步命令的 屬性:
<Button Content="Add color" Command="{Binding AddColorCommand}" IsEnabled="{Binding AddColorCommand.RunningCommandsCount.IsZero}" Grid.Row="1" />
RunningCommandsCount 是目前正在執行命令之並行異步執行數目的計數器。 只要按鍵,此計數器就會在UI線程上遞增,這可藉由系結 IsEnabled 至 RunningCommandsCount.IsZero來同步停用按鈕。
由於所有遠端 UI 命令都會以異步方式執行,因此最佳做法是一律在適當時使用 RunningCommandsCount.IsZero 來停用控件,即使命令預期會快速完成也一樣。
異步命令 和數據範本
在本節中,您會實作 [移除 ] 按鈕,讓使用者從清單中刪除專案。 我們可以為每個物件建立一個MyColor,或者我們可以在 中建立單一MyToolWindowData,並使用 參數來識別應該移除的色彩。 後者選項是更簡潔的設計,因此讓我們實作。
- 更新資料範本中的按鈕 XAML:
<Button Content="Remove" Grid.Column="2"
Command="{Binding DataContext.RemoveColorCommand,
RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListView}}}"
CommandParameter="{Binding}"
IsEnabled="{Binding DataContext.RemoveColorCommand.RunningCommandsCount.IsZero,
RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListView}}}" />
- 將 對應
AsyncCommand新增至MyToolWindowData:
[DataMember]
public AsyncCommand RemoveColorCommand { get; }
- 在建構函式
MyToolWindowData中設置命令的異步回呼。
RemoveColorCommand = new AsyncCommand(async (parameter, ancellationToken) =>
{
await Task.Delay(TimeSpan.FromSeconds(2));
Colors.Remove((MyColor)parameter!);
});
此程式代碼會使用 Task.Delay 模擬長時間運行的 異步命令。
數據內容中的參考型別
在上述程式代碼中,MyColor物件被當作異步命令的參數接收,並用作List<T>.Remove呼叫的參數,其採用參考相等性(因為 MyColor 是不會覆寫Equals的MyColor參考型別),以識別要移除的元素。 這是可能的,因為即使從UI接收參數,目前屬於資料上下文的MyColor的確切實例也會收到,而不是複製。
過程的
- 代理遠端使用者控制元件的資料上下文;
- 將更新從延伸模組傳送
INotifyPropertyChanged至 Visual Studio,反之亦然; - 將可觀察的集合更新從延伸模塊傳送至Visual Studio,反之亦然;
- 傳送 異步命令 參數
所有都尊重引用類型物件的身份。 除了字串之外,參考型別對象在傳輸回延伸模組時永遠不會重複。
在圖片中,您可以看到數據內容中每個參考類型物件(命令、集合、每個 MyColor 甚至整個數據內容)如何由遠端 UI 基礎結構指派唯一標識符。 當使用者按一下 Proxy 色彩物件的 [移除] 按鈕 #5 時,會將唯一識別碼(#5)而不是物件的值傳回擴充套件。 遠端 UI 基礎結構會負責擷取對應的 MyColor 物件,並將它當做參數傳遞至 異步命令的回呼。
具有多重綁定和事件處理的 RunningCommandsCount
如果您此時測試擴充功能,請注意,按一下其中一個移除按鈕時,會停用所有移除按鈕:
這可能是期望的行為。 但是,假設您只想要停用目前的按鈕,而且您可以讓使用者將多個色彩排入佇列以移除:我們無法使用 異步命令的 RunningCommandsCount 屬性,因為我們在所有按鈕之間共用單一命令。
我們可以藉由將一個屬性 RunningCommandsCount 附加到每個按鈕上來達成目標,這樣每種色彩都會有一個獨立的計數器。 這些功能是由 http://schemas.microsoft.com/visualstudio/extensibility/2022/xaml 命名空間提供,可讓您從 XAML 取用遠端 UI 類型:
我們將 移除 按鈕變更為以下內容:
<Button Content="Remove" Grid.Column="2"
IsEnabled="{Binding Path=(vs:ExtensibilityUICommands.RunningCommandsCount).IsZero, RelativeSource={RelativeSource Self}}">
<vs:ExtensibilityUICommands.EventHandlers>
<vs:EventHandlerCollection>
<vs:EventHandler Event="Click"
Command="{Binding DataContext.RemoveColorCommand, ElementName=RootGrid}"
CommandParameter="{Binding}"
CounterTarget="{Binding RelativeSource={RelativeSource Self}}" />
</vs:EventHandlerCollection>
</vs:ExtensibilityUICommands.EventHandlers>
</Button>
vs:ExtensibilityUICommands.EventHandlers附加屬性允許將異步命令指派給任何事件(例如 MouseRightButtonUp),而且在更進階的案例中很有用。
vs:EventHandler也可以有 CounterTarget:應該將UIElement屬性附加到vs:ExtensibilityUICommands.RunningCommandsCount,並計算與該特定事件相關的使用中執行。 系結至附加屬性時,請務必使用括弧(例如 Path=(vs:ExtensibilityUICommands.RunningCommandsCount).IsZero)。
在此情況下,我們使用 vs:EventHandler 將每個按鈕附上專屬的命令執行次數計數器。 藉由系結 IsEnabled 至附加屬性,只有在移除對應的色彩時,才會停用該特定按鈕:
使用者 XAML 資源字典
從 Visual Studio 17.10 開始,遠端 UI 支援 XAML 資源字典。 這可讓多個遠端 UI 控制件共享樣式、範本和其他資源。 它也可讓您定義不同語言的不同資源(例如字串)。
與遠端 UI 控制件 XAML 類似,資源檔案必須設定為內嵌資源:
<ItemGroup>
<EmbeddedResource Include="MyResources.xaml" />
<Page Remove="MyResources.xaml" />
</ItemGroup>
遠端 UI 會以不同於 WPF 的方式參考資源字典:它們不會新增至控件的合併字典(遠端 UI 完全不支援合併字典),但在控件的 .cs 檔案中依名稱參考:
internal class MyToolWindowContent : RemoteUserControl
{
public MyToolWindowContent()
: base(dataContext: new MyToolWindowData())
{
this.ResourceDictionaries.AddEmbeddedResource(
"MyToolWindowExtension.MyResources.xaml");
}
...
AddEmbeddedResource 會採用內嵌資源的完整名稱,預設是由專案的根命名空間、它可能位於下的任何子資料夾路徑和檔名所組成。 可以透過在專案檔中為 LogicalName 設定 EmbeddedResource,來覆蓋這類名稱。
資源檔案本身是一般的 WPF 資源字典:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib">
<system:String x:Key="removeButtonText">Remove</system:String>
<system:String x:Key="addButtonText">Add color</system:String>
</ResourceDictionary>
您可以在遠端 UI 控制件中使用 DynamicResource 來參考資源字典中的資源。
<Button Content="{DynamicResource removeButtonText}" ...
本地化 XAML 資源字典
遠端 UI 資源字典可以與當地語系化內嵌資源的方式相同:您可以建立其他具有相同名稱和語言後綴的 XAML 檔案,例如 MyResources.it.xaml 義大利資源:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib">
<system:String x:Key="removeButtonText">Rimuovi</system:String>
<system:String x:Key="addButtonText">Aggiungi colore</system:String>
</ResourceDictionary>
您可以使用項目檔中的通配符,將所有本地化的 XAML 字典納入為內嵌資源:
<ItemGroup>
<EmbeddedResource Include="MyResources.*xaml" />
<Page Remove="MyResources.*xaml" />
</ItemGroup>
在數據內容中使用 WPF 類型
到目前為止, 遠端使用者控件 的數據內容已由基本類型(數位、字串串等)、可觀察的集合和標記為的自有類別 DataContract所組成。 有時候,將簡單的 WPF 類型(例如複合筆刷)包含在資料上下文中可能會很有用。
因為 VisualStudio.Extensibility 擴充功能 甚至無法在 Visual Studio 進程中執行,所以它無法直接與其 UI 共用 WPF 物件。 擴充功能可能甚至無法存取 WPF 類型,因為它可以設定目標 netstandard2.0 或 net6.0 (不是 -windows 變體)。
遠端 UI 提供 XamlFragment 類型,允許在 遠端使用者控制件的數據內容中包含 WPF 物件的 XAML 定義:
[DataContract]
public class MyColor
{
public MyColor(byte r, byte g, byte b)
{
ColorText = $"#{r:X2}{g:X2}{b:X2}";
Color = new(@$"<LinearGradientBrush xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
StartPoint=""0,0"" EndPoint=""1,1"">
<GradientStop Color=""Black"" Offset=""0.0"" />
<GradientStop Color=""{ColorText}"" Offset=""0.7"" />
</LinearGradientBrush>");
}
[DataMember]
public string ColorText { get; }
[DataMember]
public XamlFragment Color { get; }
}
使用上述程式代碼時, Color 屬性值會 LinearGradientBrush 轉換成數據內容 Proxy 中的物件: 
遠端 UI 和線程
Async command 的回呼(以及透過資料繫結由 UI 更新值的回呼)會在隨機選擇的執行緒集區線程上引發。 回呼會逐一引發,並且在程式代碼讓出控制權之前不會重疊(使用 await 表達式)。
將 NonConcurrentSynchronizationContext 傳遞至 RemoteUserControl 建構函式,即可變更此行為。 在此情況下,您可以對與該控制項相關的所有 異步命令 和 INotifyPropertyChanged 回調使用提供的同步處理內容。