版本調適型程式碼

您可以考慮撰寫調適型程式碼的方式,類似於建立調適型 UI。 您可以設計基本 UI 以利在最小的螢幕上執行,然後在偵測到應用程式在較大的螢幕上執行時移動或新增元素。 使用調適型程式碼時,您會撰寫基本程式碼,以利在最低 OS 版本上執行,而且當您偵測到您的應用程式是在較新版本上執行時,您可以新增手動選取的功能。

如需有關 ApiInformation、API 協定以及設定 Visual Studio 的重要背景資訊,請參閱版本調適型應用程式

執行階段 API 檢查

您可以使用程式碼中條件中的 Windows.Foundation.Metadata.ApiInformation 類別來測試您想要呼叫的 API 是否存在。 無論您的應用程式於何處執行,都會評估此條件,但只會在 API 存在且可供呼叫的裝置上評估為 true 。 這可讓您撰寫版本調適型程式碼,以便建立使用僅在特定 OS 版本提供之 API 的應用程式。

這裡我們要看看以 Windows Insider Preview 中的新功能為目標的特定範例。 如需使用 ApiInformation 的一般概觀,請參閱使用延伸功能 SDK 進行程式設計,以及使用 API 協定動態偵測功能

提示

許多執行階段 API 檢查可能會影響應用程式的效能。 我們會在這些範例中內嵌顯示檢查。 在生產程式碼中,您應該執行檢查一次並快取結果,然後在應用程式中使用快取的結果。

不支援的情節

在大部分情況下,您可以將應用程式的最低版本設定為 SDK 10240 版本,並使用執行階段檢查在更新版本上執行應用程式時啟用任何新的 API。 不過,在某些情況下,您必須增加應用程式的最低版本,才能使用新功能。

如果您使用以下請況,則必須增加應用程式的最低版本:

  • 需要舊版中不提供的功能的新 API。 您必須將支援的最低版本增加到包含該功能的版本。 如需詳細資訊,請參閱應用程式功能宣告
  • 任何新增至 generic.xaml 且無法在舊版中使用的新資源索引鍵。 執行階段所使用的 generic.xaml 版本是由裝置執行的作業系統版本決定。 您無法使用執行階段 API 檢查來判斷 XAML 資源是否存在。 因此,您只能使用應用程式支援的最低版本中可用的資源索引鍵,否則 XAMLParseException 將導致您的應用程式在執行階段當機。

調適型程式碼選項

建立調適型程式碼的方式有兩種。 在大部分案例中,您會撰寫應用程式標記以在最低版本上執行,然後在出現時,使用您的應用程式程式碼來點選較新的作業系統功能。 不過,如果您需要在視覺狀態中更新屬性,而且作業系統版本之間只有屬性或列舉值變更,您可以依據 API 的存在來建立可延伸狀態觸發程序。

此處我們會比較這些選項。

應用程式程式碼

使用時機:

  • 建議用於所有調適型程式碼案例,但針對可延伸觸發程序定義的特定案例除外。

優點:

  • 避免開發人員將 API 差異繫結到標記中的額外負荷/複雜度。

缺點:

  • 沒有設計工具支援。

狀態觸發程序

使用時機:

  • 當作業系統版本之間僅存在不需要邏輯變更的屬性或列舉變更並且連線到視覺狀態時使用。

優點:

  • 可讓您依據 API 的存在來建立觸發的特定視覺狀態。
  • 有些設計工具支援可用。

缺點:

  • 自訂觸發程序的使用僅限於視覺狀態,這本身並不適用於複雜的調適型配置。
  • 必須使用 Setter 來指定值變更,因此只能進行簡單的變更。
  • 自訂狀態觸發程序的設定和使用相當詳細。

調適型程式碼範例

在本節中,我們會展示幾個使用 Windows 10 版本 1607 (Windows 測試人員預覽) 中新增 API 的調適型程式碼範例。

範例 1:新列舉值

Windows 10 版本 1607 會將新值新增至 InputScopeNameValue 列舉:ChatWithoutEmoji。 這個新的輸入範圍具有與聊天輸入範圍相同的輸入行為 (拼字檢查、自動完成、自動大寫),但它會對應到沒有表情符號按鈕的觸控鍵盤。 如果您建立自己的表情符號選擇器,而且想要停用觸控鍵盤中的內建表情符號按鈕,這會很有用。

此範例示範如何檢查 ChatWithoutEmoji 列舉值是否存在,並設定 TextBoxInputScope 屬性 (如果存在)。 如果執行應用程式的系統上不存在該輸入範圍,則將 InputScope 設定為聊天。 顯示的程式碼可以放在 Page 建構函式或 Page.Loaded 事件處理常式中。

提示

檢查 API 時,請使用靜態字串,而不是依賴 .NET 語言功能,否則應用程式可能會嘗試存取未定義的類型,並在執行階段當機。

C#

// Create a TextBox control for sending messages 
// and initialize an InputScope object.
TextBox messageBox = new TextBox();
messageBox.AcceptsReturn = true;
messageBox.TextWrapping = TextWrapping.Wrap;
InputScope scope = new InputScope();
InputScopeName scopeName = new InputScopeName();

// Check that the ChatWithEmoji value is present.
// (It's present starting with Windows 10, version 1607,
//  the Target version for the app. This check returns false on earlier versions.)         
if (ApiInformation.IsEnumNamedValuePresent("Windows.UI.Xaml.Input.InputScopeNameValue", "ChatWithoutEmoji"))
{
    // Set new ChatWithoutEmoji InputScope if present.
    scopeName.NameValue = InputScopeNameValue.ChatWithoutEmoji;
}
else
{
    // Fall back to Chat InputScope.
    scopeName.NameValue = InputScopeNameValue.Chat;
}

// Set InputScope on messaging TextBox.
scope.Names.Add(scopeName);
messageBox.InputScope = scope;

// For this example, set the TextBox text to show the selected InputScope.
messageBox.Text = messageBox.InputScope.Names[0].NameValue.ToString();

// Add the TextBox to the XAML visual tree (rootGrid is defined in XAML).
rootGrid.Children.Add(messageBox);

在上一個範例中,會建立 TextBox,而且所有屬性都會在程式碼中設定。 但是,如果您有現有的 XAML,並且只需要在支援新值的系統上變更 InputScope 屬性,則無需變更 XAML 即可執行此操作,如此處所示。 您可以在 XAML 中將預設值設定為聊天,但如果 ChatWithoutEmoji 值存在,則會在程式碼中覆寫此值。

XAML

<TextBox x:Name="messageBox"
         AcceptsReturn="True" TextWrapping="Wrap"
         InputScope="Chat"
         Loaded="messageBox_Loaded"/>

C#

private void messageBox_Loaded(object sender, RoutedEventArgs e)
{
    if (ApiInformation.IsEnumNamedValuePresent("Windows.UI.Xaml.Input.InputScopeNameValue", "ChatWithoutEmoji"))
    {
        // Check that the ChatWithEmoji value is present.
        // (It's present starting with Windows 10, version 1607,
        //  the Target version for the app. This code is skipped on earlier versions.)
        InputScope scope = new InputScope();
        InputScopeName scopeName = new InputScopeName();
        scopeName.NameValue = InputScopeNameValue.ChatWithoutEmoji;
        // Set InputScope on messaging TextBox.
        scope.Names.Add(scopeName);
        messageBox.InputScope = scope;
    }

    // For this example, set the TextBox text to show the selected InputScope.
    // This is outside of the API check, so it will happen on all OS versions.
    messageBox.Text = messageBox.InputScope.Names[0].NameValue.ToString();
}

既然我們有具體的範例,讓我們查看目標與最低版本設定如何套用至它。

在這些範例中,您可以使用 XAML 中的聊天列舉值,或在程式碼中不使用檢查,因為它存在於最低支援的作業系統版本中。

如果您在 XAML 中使用 ChatWithoutEmoji 值,或在程式碼中沒有使用檢查,則會進行編譯而不會發生錯誤,因為它存在於目標作業系統版本中。 它也會在具有目標作業系統版本的系統上執行,而不會發生錯誤。 不過,當應用程式使用最低版本在作業系統上執行時,它會在執行階段當機,因為 ChatWithoutEmoji 列舉值不存在。 因此,您只能在程式碼中使用此值,並將它包裝在執行階段 API 檢查中,因此只有在目前系統上支援時才呼叫此值。

範例 2:新控制項

新版本的 Windows 通常會將新的控制項帶入 UWP API 介面,以將新功能帶入平台。 若要利用新的控制項,請使用 ApiInformation.IsTypePresent \(英文\) 方法。

Windows 10 版本 1607 引進了名為 MediaPlayerElement的新媒體控制項。 此控制項組建在 MediaPlayer 類別上,因此它帶來了功能,例如輕鬆地繫結至背景音訊的功能,並利用媒體堆疊中的架構改善。

但是,如果應用程式在執行 Windows 10 版本 (比 1607 版本舊)的裝置上執行,則必須使用 MediaElement 控制項,而不是新的 MediaPlayerElement 控制項。 您可以使用 ApiInformation.IsTypePresent 方法來檢查執行階段是否有 MediaPlayerElement 控制項是否存在,並載入適用於執行應用程式之系統的控制項。

此範例示範如何依據 MediaPlayerElement 類型是否存在,建立使用新 MediaPlayerElement 或舊 MediaElement 的應用程式。 在此程式碼中,您可以使用 UserControl 類別來組件化控制項及其相關的 UI 和程式碼,以便您可以依據作業系統版本來切換它們。 或者,您可以使用自訂控制項,其提供的功能和自訂行為比這個簡單範例所需的功能與自訂行為還要多。

MediaPlayerUserControl

MediaPlayerUserControl封裝一個 MediaPlayerElement 和數個按鈕,用來逐一跳過媒體畫面。 UserControl 可讓您將這些控制項視為單一實體,並可讓您更輕鬆地在較舊的系統上使用 MediaElement 切換。 此使用者控制項應該只用於 MediaPlayerElement 存在的系統上,因此您不會在此使用者控制項內的程式碼中使用 ApiInformation 檢查。

注意

為了保持此範例簡單且專注,框架步驟按鈕會放在媒體播放器之外。 為了獲得更好的使用者體驗,您應該自訂 MediaTransportControls 以包含您的自訂按鈕。 如需詳細資訊,請參閱自訂傳輸控制項

XAML

<UserControl
    x:Class="MediaApp.MediaPlayerUserControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:MediaApp"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300"
    d:DesignWidth="400">

    <Grid x:Name="MPE_grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <StackPanel Orientation="Horizontal" 
                    HorizontalAlignment="Center" Grid.Row="1">
            <RepeatButton Click="StepBack_Click" Content="Step Back"/>
            <RepeatButton Click="StepForward_Click" Content="Step Forward"/>
        </StackPanel>
    </Grid>
</UserControl>

C#

using System;
using Windows.Media.Core;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace MediaApp
{
    public sealed partial class MediaPlayerUserControl : UserControl
    {
        public MediaPlayerUserControl()
        {
            this.InitializeComponent();
            
            // The markup code compiler runs against the Minimum OS version so MediaPlayerElement must be created in app code
            MPE = new MediaPlayerElement();
            Uri videoSource = new Uri("ms-appx:///Assets/UWPDesign.mp4");
	        MPE.Source = MediaSource.CreateFromUri(videoSource);
	        MPE.AreTransportControlsEnabled = true;
            MPE.MediaPlayer.AutoPlay = true;

            // Add MediaPlayerElement to the Grid
            MPE_grid.Children.Add(MPE);

        }

        private void StepForward_Click(object sender, RoutedEventArgs e)
        {
            // Step forward one frame, only available using MediaPlayerElement.
            MPE.MediaPlayer.StepForwardOneFrame();
        }

        private void StepBack_Click(object sender, RoutedEventArgs e)
        {
            // Step forward one frame, only available using MediaPlayerElement.
            MPE.MediaPlayer.StepForwardOneFrame();
        }
    }
}

MediaElementUserControl

MediaElementUserControl封裝一個 MediaElement 控制項。

XAML

<UserControl
    x:Class="MediaApp.MediaElementUserControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:MediaApp"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300"
    d:DesignWidth="400">

    <Grid>
        <MediaElement AreTransportControlsEnabled="True" 
                      Source="Assets/UWPDesign.mp4"/>
    </Grid>
</UserControl>

注意

MediaElementUserControl 的程式碼頁面只包含產生的程式碼,因此不會顯示。

根據 IsTypePresent 初始化控制項

在執行階段,您會呼叫 ApiInformation.IsTypePresent 來檢查 MediaPlayerElement。 如果存在,您會載入 MediaPlayerUserControl,如果不是,則會載入 MediaElementUserControl

C#

public MainPage()
{
    this.InitializeComponent();

    UserControl mediaControl;

    // Check for presence of type MediaPlayerElement.
    if (ApiInformation.IsTypePresent("Windows.UI.Xaml.Controls.MediaPlayerElement"))
    {
        mediaControl = new MediaPlayerUserControl();
    }
    else
    {
        mediaControl = new MediaElementUserControl();
    }

    // Add mediaControl to XAML visual tree (rootGrid is defined in XAML).
    rootGrid.Children.Add(mediaControl);
}

重要

請記住,這項檢查只會將 mediaControl 物件設定為 MediaPlayerUserControlMediaElementUserControl。 您必須在程式碼中的任何其他位置執行這些條件式檢查,以判斷是否要使用 MediaPlayerElement 或 MediaElement API。 您應該執行檢查一次並快取結果,然後在應用程式中使用快取的結果。

狀態觸發程序範例

可延伸狀態觸發程序可讓您使用標記和程式碼,依據您簽入程式碼的條件來觸發視覺狀態變更;在此案例中,存在特定 API。 我們不建議在常見調適型程式碼案例中使用狀態觸發程序,因為涉及額外負荷,以及僅限制視覺狀態。

只有在不同的作業系統版本之間有小型 UI 變更而不會影響其餘 UI 時,才應該針對調適型程式碼使用狀態觸發程序,例如控制項上的屬性或列舉值變更。

範例 1:新屬性

設定可延伸狀態觸發程式的第一個步驟是子類別化 StateTriggerBase 類別,以利依據 API 的存在建立作用中的自訂觸發程序。 這個範例顯示如果屬性存在符合 XAML 中所設定的 _isPresent 變數,就會啟動的觸發程序。

C#

class IsPropertyPresentTrigger : StateTriggerBase
{
    public string TypeName { get; set; }
    public string PropertyName { get; set; }

    private Boolean _isPresent;
    private bool? _isPropertyPresent = null;

    public Boolean IsPresent
    {
        get { return _isPresent; }
        set
        {
            _isPresent = value;
            if (_isPropertyPresent == null)
            {
                // Call into ApiInformation method to determine if property is present.
                _isPropertyPresent =
                ApiInformation.IsPropertyPresent(TypeName, PropertyName);
            }

            // If the property presence matches _isPresent then the trigger will be activated;
            SetActive(_isPresent == _isPropertyPresent);
        }
    }
}

下一個步驟是在 XAML 中設定視覺狀態觸發程序,以利依據 API 的存在而產生兩個不同的視覺狀態。

Windows 10 版本 1607 在 FrameworkElement \(英文\) 類別上導入了新的屬性,稱為 AllowFocusOnInteraction \(英文\),它可判斷使用者與控制項互動時,控制項是否會接受焦點。 如果您想要在使用者按一下按鈕時,將專注放在資料輸入的文字方塊上 (並讓觸控鍵盤保持顯示),這會很有用。

此範例中的觸發程序會檢查屬性是否存在。 如果屬性存在,它會將按鈕上的 AllowFocusOnInteraction 屬性設定為 false;如果屬性不存在,則按鈕會保留其原始狀態。 TextBox 包含在內,讓您在執行程式碼時更容易看到此屬性的效果。

XAML

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <StackPanel>
        <TextBox Width="300" Height="36"/>
        <!-- Button to set the new property on. -->
        <Button x:Name="testButton" Content="Test" Margin="12"/>
    </StackPanel>

    <VisualStateManager.VisualStateGroups>
        <VisualStateGroup x:Name="propertyPresentStateGroup">
            <VisualState>
                <VisualState.StateTriggers>
                    <!--Trigger will activate if the AllowFocusOnInteraction property is present-->
                    <local:IsPropertyPresentTrigger 
                        TypeName="Windows.UI.Xaml.FrameworkElement" 
                        PropertyName="AllowFocusOnInteraction" IsPresent="True"/>
                </VisualState.StateTriggers>
                <VisualState.Setters>
                    <Setter Target="testButton.AllowFocusOnInteraction" 
                            Value="False"/>
                </VisualState.Setters>
            </VisualState>
        </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
</Grid>

範例 2:新列舉值

這個範例示範如何依據值是否存在來設定不同的列舉值。 它會使用自訂狀態觸發程序來達成與先前聊天範例相同的結果。 在此範例中,如果裝置執行的是 Windows 10 版本 1607,則使用新的 ChatWithoutEmoji 輸入範圍,否則使用聊天輸入範圍。 使用此觸發程序的視覺狀態是在 if-else 樣式中設定,其中輸入範圍是依據新列舉值的存在而選擇的。

C#

class IsEnumPresentTrigger : StateTriggerBase
{
    public string EnumTypeName { get; set; }
    public string EnumValueName { get; set; }

    private Boolean _isPresent;
    private bool? _isEnumValuePresent = null;

    public Boolean IsPresent
    {
        get { return _isPresent; }
        set
        {
            _isPresent = value;

            if (_isEnumValuePresent == null)
            {
                // Call into ApiInformation method to determine if value is present.
                _isEnumValuePresent =
                ApiInformation.IsEnumNamedValuePresent(EnumTypeName, EnumValueName);
            }

            // If the property presence matches _isPresent then the trigger will be activated;
            SetActive(_isPresent == _isEnumValuePresent);
        }
    }
}

XAML

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

    <TextBox x:Name="messageBox"
     AcceptsReturn="True" TextWrapping="Wrap"/>


    <VisualStateManager.VisualStateGroups>
        <VisualStateGroup x:Name="EnumPresentStates">
            <!--if-->
            <VisualState x:Name="isPresent">
                <VisualState.StateTriggers>
                    <local:IsEnumPresentTrigger 
                        EnumTypeName="Windows.UI.Xaml.Input.InputScopeNameValue" 
                        EnumValueName="ChatWithoutEmoji" IsPresent="True"/>
                </VisualState.StateTriggers>
                <VisualState.Setters>
                    <Setter Target="messageBox.InputScope" Value="ChatWithoutEmoji"/>
                </VisualState.Setters>
            </VisualState>
            <!--else-->
            <VisualState x:Name="isNotPresent">
                <VisualState.StateTriggers>
                    <local:IsEnumPresentTrigger 
                        EnumTypeName="Windows.UI.Xaml.Input.InputScopeNameValue" 
                        EnumValueName="ChatWithoutEmoji" IsPresent="False"/>
                </VisualState.StateTriggers>
                <VisualState.Setters>
                    <Setter Target="messageBox.InputScope" Value="Chat"/>
                </VisualState.Setters>
            </VisualState>
        </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
</Grid>