December 2017
Volume 32 Number 12
ユニバーサル Windows プラットフォーム - Windows 10 の新しいハンバーガー メニューの開発者向けガイド
Windows XAML チームは Windows 10 Fall Creators Update で NavigationView コントロールをリリースしました。このコントロールがリリースされるまで、ハンバーガー メニューの実装に取り組む開発者は、SplitView コントロールの初歩的な機能しか利用できませんでした。結果として得られるインターフェイスは表示と動作のどちらにも一貫性がありませんでした。ファーストパーティの Microsoft 製アプリ (Groove、Xbox、ニュース、メールなど) でさえ、ポートフォリオ間の見た目を揃えるのに四苦八苦しました。今回紹介する NavigationView のように、社内の問題が社外向けソリューションを促すことはよくあります。
このコントロールは、XAML の開発者に新たな美しいビジュアルを提供します。これは、Microsoft の新しい Fluent デザイン システムのように、アダプティブな拡大縮小、ローカライズ、アクセシビリティ、署名といった Windows エクスペリエンスが包括的にサポートされるデバイス間で一貫性をもって実装されます。コントロールは美しく、エンド ユーザーはコントロールの選択アニメーションを繰り返し呼び出すだけで、生産性にとって重要な時間を失うことがなくなります。これは魅力的です。図 1 は NavigationView の基本スタイルを示しています。
図 1 NavigationView の基本スタイル
基礎
NavigationView をアプリに追加するのは簡単です。通常、NavigationView は ShellPage.xaml のような専用ページになります。数行の XAML だけで、ユーザーの通常操作に応答するメニュー、ボタン、およびハンドラーが提供されます。次のコードは MenuItems の NavigationViewItem を示しています。
<NavigationView SelectionChanged="SelectionChanged">
<NavigationView.MenuItems>
<NavigationViewItemHeader Content="Section A" />
<NavigationViewItem Content="Item 01" />
<NavigationViewItem Content="Item 02" />
</NavigationView.MenuItems>
<Frame x:Name="NavigationFrame" />
</NavigationView>
これらは主なナビゲーション ボタンです。サポートするその他の項目として、NavigationViewItemHeader と NavigationViewItemSeparator があります。これらを併せて使用することで、開発者は美しく洗練されたメニューを作成できます。NavigationView の使用時に考慮すべき事項がいくつかあります。
構造 NavigationView のパーツを図 2 に示します。各領域によって包括的な UX が生み出されます。Header (ヘッダー)、Pane Footer (ウィンドウ フッター)、Auto Suggest (自動候補検索)、および Settings (設定) ボタンは、アプリの設計要件に応じて省略できます。
図 2 NavigationView のパーツ
モード コントロールには、3 つのモード Minimal (最小)、Compact (コンパクト)、および Extended (拡大) があります (図 3 参照)。各モードは、しきい値が設定された組み込みのカスタマイズ可能なビューに基づいて自動選択されます。これらのモードにより、アプリやデバイスのサイズが変更されても、NavigationView は有益で実用的な状態が保たれます。
図 3 NavigationView のモード
現実の問題
NavigationView を理解するのは簡単です。ですが、実装は簡単になるとは限りません。特に洗練された現実のシナリオのコンテキストでは簡単ではありません。開発者が直面するあらゆるユース ケースにコントロールを対応させるには、多くの場合、しっかりと頭を働かせてコーディングする必要があります。ここからは、開発者が認識しておくべき問題点と要素の概要を示します。その対処方法については、後半で取り上げます。
データ バインディング 個人的には、最上位レベルのナビゲーション コントロールにデータをバインドするメニュー項目を配置するのはとんでもないことだ思っています。でも、開発者がすべて同じ考えではないこともわかっています。そのため、分離コードから NavigationView に簡単にバインドできるようになっています。ビュー モデルからバインドするには、UI 名前空間を参照するといった基本ルールに違反する必要があります。
ナビゲート 開発者は、NavigationView がナビゲートを行わないことに驚くかもしれません。NavigationView はナビゲーションの視覚的なアフォーダンスに過ぎません。Frame がなく、メニュー項目が実行すべき動作も理解しません。開発者が最初に解決する必要があるのは、ページの再読み込みに関する何らかのロジックを備えたシンプルなナビゲーションです。
戻るボタン Windows 10 にはシェルによって戻されるボタンがあります。これは省略できる場合もありますが、タブレット モードなどのモードでは必須です。戻るボタンはキャンバス上の領域を節約して、戻るナビゲーション用に統一されたポイントを確立します。ユニバーサル WinRT の BackRequested イベントにアタッチするのは簡単ですが、NavigationView の選択内容を同期するという要件もあります。
設定ボタン NavigationView はメニュー ウィンドウの下部に、ローカライズされる既定の設定ボタンが表示されます。これ自体は素晴らしいことです。このボタンによって、共通のユーザー操作を呼び出す 1 つの標準ポイントが確立されます。デザイナーにも開発者にも、エコシステム全体で見た目が揃った UX を用意するために、こうした共通操作を習得して迅速に採用することをお勧めします。
設定ボタンの実装は簡単かつ明確です。ただし、既定では十分な機能を提供しないことが NavigationView のもう 1 つの要件になります。問題は、すべての XAML 開発者がコントロールの動作をコーディングではなく、宣言によって決めたいと望んでいることにあります。
ヘッダー項目 NavigationView の MenuItem プロパティは、縦線ボタンの表示に使用される NavigationViewItemHeader オブジェクトを受け取ります。これが特に役立つのは、NavigationViewItems をパーティションに分割するときです。しかし、NavigationView のメニュー ウィンドウを開いて閉じると、ヘッダーのコンテンツが切り詰められます。メニューの外観と構造を、狭い幅と広い幅の両方のモードで制御できるようにするのは開発者の役割です。
現実の解決策
XAML の開発者には、問題を解決する手段がいくつかあります。コントロールを継承することで、開発者はそのコントロールの動作 (bit.ly/2gQ4vN4、英語) を拡張して、拡張メソッドでシールド コントロール (/ja-jp/dotnet/csharp/programming-guide/classes-and-structs/extension-methods) の基本実装を強化することができます。また、添付プロパティでも、コントロールの機能を拡張できます (bit.ly/2giDGAn、英語)。こちらは、XAML でのサポート宣言もあります。
データ バインディング XAML チームがデータ バインディングをあみだした 2006 年以降、モデル - ビュー - ビューモデル (MVVM: Model-View-ViewModel) は Microsoft 製のファーストパーティ アプリを含め、XAML 開発者のお気に入りのパターンになっています。設計パターンの原則の 1 つは、ビューモデルに依存せず、ビューモデルで UI 名前空間を参照しないことです。これが優れている理由はたくさんあります。次のコード スニペットに示すように、NavigationView は、ListView.ItemsSource と同様、MenuItemsSource プロパティへの NavigationViewItems のデータ バインディングをサポートしますが、UI 名前空間は使用できません。分離コードでは問題ありませんが、ビューモデルの場合、解決する際に問題となります。
public IEnumerable<object> MenuItems
{
get
{
return new[]
{
new NavigationViewItem { Content = "Home" },
new NavigationViewItem { Content = "Reports" },
new NavigationViewItem { Content = "Calendar" },
};
}
}
ここではビューモデルで Windows.UI.Xaml.Controls を参照しないようにするため、NavigationViewItem を DTO に抽象化しています。使用する可能性のあるピア オブジェクトごとに、このプロセスを繰り返します。各項目の順序を担当するのはビューモデルで、ビュー ロジックによって管理する必要があります。次のコードに示すように、これらの抽象化はビュー モデルでシンプルかつ簡単に提供できます。
public class NavItemEx
{
public string Icon { get; set; }
public string Text { get; set; }
}
public class NavItemHeaderEx
{
public string Text { get; set; }
}
public class NavItemSeparatorEx { }
ただし、NavigationView はこのカスタム クラスを認識しないため、このカスタム クラスをレンダリング用に適切な NavigationView コントロールに変換する必要があります。カスタム クラスにバインドするには、レンダリングを強制実行するための大量のカスタム コードが NavigationView に必要になります。これは避けたいところです。注: ここでは意図的にカスタム テンプレートの使用を避けているため、誤ってアクセシビリティを台無しにすることも、今後のプラットフォーム リリースでのテンプレートの機能強化を見逃すこともありません。変換を簡単にするため、XAML バインディングで参照できる値コンバーターを導入します。図 4 は、列挙可能なカスタム クラスを受け取って、NavigationView が想定するオブジェクトを返すコードを示しています。
図 4 NavItem 用コンバーター
public class INavConverter : IvalueConverter
{
public object Convert(object v, Type t, object p, string l)
{
var list = new List<object>();
foreach (var item in (v as Ienumerable<object>))
{
switch (item)
{
case NavItemEx dto:
list.Add(ToItem(dto));
break;
case NavItemHeaderEx dto:
list.Add(ToItem(dto));
break;
case NavItemSeparatorEx dto:
list.Add(ToItem(dto));
break;
}
}
return list;
}
object IvalueConverter.ConvertBack(object v, Type t, object p, string l)
throw new NotImplementedException();
NavigationViewItem ToItem(NavItemEx item)
new NavigationViewItem
{
Content = item.Text,
Icon = ToFontIcon(item.Icon),
};
FontIcon ToFontIcon(string glyph)
new FontIcon { Glyph = glyph, };
NavigationViewItemHeader ToItem(NavItemHeaderEx item)
new NavigationViewItemHeader { Content = item.Text, };
NavigationViewItemSeparator ToItem(NavItemSeparatorEx item)
new NavigationViewItemSeparator { };
}
このコンバーターをアプリ単位またはページレベルのリソースとして参照した後の構文は、他のコンバーターと同様シンプルになります。最上位レベルのナビゲーションでデータ バインドすることがどれほど突拍子もないことであるかを時間をかけて繰り返しお伝えしたいのですが、この拡張可能な解決策は次のようにシームレスに作用します。
MenuItemsSource=”{x:Bind ViewModel.Items, Converter={StaticResource NavConverter}}”
ナビゲーション ユニバーサル Windows プラットフォーム (UWP) でのナビゲーションは XAML の Frame から始まります。しかし、NavigationView には Frame がありません。さらに、メニュー ボタンの目的 (つまり、メニュー ボタンから開くページ) を宣言する方法がありません。これは、次に示す XAML の添付プロパティによって簡単に解決されます。
public partial class NavProperties : DependencyObject
{
public static Type GetPageType(NavigationViewItem obj)
=> (Type)obj.GetValue(PageTypeProperty);
public static void SetPageType(NavigationViewItem obj, Type value)
=> obj.SetValue(PageTypeProperty, value);
public static readonly DependencyProperty PageTypeProperty =
DependencyProperty.RegisterAttached("PageType", typeof(Type),
typeof(NavProperties), new PropertyMetadata(null));
}
NavigationViewItem に PageType を用意したら、XAML でターゲット ページを宣言するか、先ほどのビューモデルにバインドできます。注: 設計上必要であれば、さらに Parameter プロパティと TransitionInfo プロパティを追加できます。ただし、このサンプルでは Navigation の基本実装に注目します。次に、図 5 に示すように、拡張した NavigationView でナビゲーションを処理します。
図 5 NavViewEx (拡張した NavigationView)
public class NavViewEx : NavigationView
{
Frame _frame;
public Type SettingsPageType { get; set; }
public NavViewEx()
{
Content = _frame = new Frame();
_frame.Navigated += Frame_Navigated;
ItemInvoked += NavViewEx_ItemInvoked;
SystemNavigationManager.GetForCurrentView()
.BackRequested += ShellPage_BackRequested;
}
private void NavViewEx_ItemInvoked(NavigationView sender,
NavigationViewItemInvokedEventArgs args)
{
if (args.IsSettingsInvoked)
SelectedItem = SettingsItem;
else
SelectedItem = Find(args.InvokedItem.ToString());
}
private void Frame_Navigated(object sender, NavigationEventArgs e)
=> SelectedItem = (e.SourcePageType == SettingsPageType)
? SettingsItem : Find(e.SourcePageType) ?? base.SelectedItem;
private void ShellPage_BackRequested(object sender, BackRequestedEventArgs e)
=> _frame.GoBack();
NavigationViewItem Find(string content)
=> MenuItems.OfType<NavigationViewItem>()
.SingleOrDefault(x => x.Content.Equals(content));
NavigationViewItem Find(Type type)
=> MenuItems.OfType<NavigationViewItem>()
.SingleOrDefault(x => type.Equals(x.GetValue(NavProperties.PageTypeProperty)));
public virtual void Navigate(Frame frame, Type type)
=> frame.Navigate(type);
public new object SelectedItem
{
set
{
if (value == SettingsItem)
{
Navigate(_frame, SettingsPageType);
base.SelectedItem = value;
_frame.BackStack.Clear();
}
else if (value is NavigationViewItem i && i != null)
{
Navigate(_frame, i.GetValue(NavProperties.PageTypeProperty) as Type);
base.SelectedItem = value;
_frame.BackStack.Clear();
}
UpdateBackButton();
}
}
private void UpdateBackButton()
{
SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility =
(_frame.CanGoBack) ? AppViewBackButtonVisibility.Visible
: AppViewBackButtonVisibility.Collapsed;
}
}
図 5 を見ると、4 つの重要な機能強化が行われているのがわかります。まず、コントロールのインスタンスを作成中に XAML Frame が挿入されます。次に、Frame.Navigated、ItemInvoked、および BackRequested 用のハンドラーが追加されています。さらに、SelectedItem がオーバーライドされ、BackStack ロジックと BackButton ロジックが追加されています。最後に、新しい SettingsPageType プロパティがクラスに追加されています。
戻るボタン 新しい明示的な Frame は便利なだけではなく、Navigation イベントのソースが提供されます。これは重要です。NavigationView によってナビゲーションが呼び出されたときに、シェルによって戻されるボタンの表示を更新します。ただし、ユーザーが別の方法をナビゲートした場合、何らかのイベントがないと戻るボタンを更新することを認識できません。Frame.Navigated イベントは、優れたグローバルな選択肢になります。
検索 NavigationView の ItemInvoked イベントの予期しない動作は、カスタム イベント引数に渡される InvokedItem プロパティが NavigationViewItem の文字列のコンテンツであって、項目自体へのオブジェクト参照ではないことで生じます。そのため、このカスタマイズしたコントロールの Find メソッドでは、ItemInvoked イベントに渡されるコンテンツまたは Frame.Navigated イベントに渡される PageType に基づいて適切な NavigationViewItem を見つけます。
NavigationViewItem のコンテンツがデバイスのローカライズ設定によって動的に変化する可能性があることに注意が必要です。オンライン ドキュメント (bit.ly/2xQodCM、英語) に示されているように、ハードコーディングされた switch ステートメントによる ItemInvoked の処理は、英語圏のユーザーのみに有効です。そのため、UWP アプリをサポートするために言語を追加する場合は、適切な switch ステートメントが必要になります。コード内にマジック ナンバーやマジック文字列を含めてはいけません。多くのコード ベースと互換性がなくなります。
設定 設定ボタンは、NavigationView の選択ロジックに含まれる唯一のボタンで、メニュー ウィンドウの下部にあります。このボタンを押すと、ユーザーは設定ページに移動します。その実装を簡略化するには、SettingsPageType カスタム プロパティに注目します。このプロパティは設定に必要なターゲット ページの種類を保持します。オーバーライドした SelectedItem セッターは、設定ボタンをテストし、その結果に応じて、宣言どおりにナビゲートします。
NavigationViewItem の PageType プロパティまたは SettingsPageType プロパティで処理されないのは、Frame の Navigate メソッドにカスタムの TransitionInfo を指定して、ナビゲーション中に切り替え情報を強制的に取得する方法です。これはアプリにとって重要なカスタマイズになる可能性があるため、カスタム プロパティまたは添付プロパティを追加して、この追加の指示に対処します。これを実現するコードを次に示します。
<local:NavViewEx SettingsPageType="views:SettingsPage">
<NavigationView.MenuItems>
<NavigationViewItem Content="Item 01"
local:NavProperties.PageType="views:Page01" />
<NavigationViewItem Content="Item 02"
local:NavProperties.PageType="views:Page02" />
<NavigationViewItem Content="Item 03"
local:NavProperties.PageType="views:Page03" />
</NavigationView.MenuItems>
</local:NavViewEx>
このような拡張性により、開発者はコントロールとクラスの動作を積極的に拡張できます。その際、根底となる実装を変更する必要はありません。それは長年にわたって C# と XAML に存在する機能で、コーディングの構文を簡潔にし、XAML の宣言をシンプルにします。また、直観的なアプローチなので、他の開発者は少し説明を受けるだけで明確に理解できます。
スタート ページ アプリが読み込まれたときに、最初はメニュー項目は呼び出されません。下記のように別の添付プロパティを追加すると、XAML で目的を宣言できるため、拡張した NavigationView はその Frame で最初のページを初期化できます。プロパティは次のようになります。
public partial class NavProperties : DependencyObject
{
public static bool GetIsStartPage(NavigationViewItem obj)
=> (bool)obj.GetValue(IsStartPageProperty);
public static void SetIsStartPage(NavigationViewItem obj, bool value)
=> obj.SetValue(IsStartPageProperty, value);
public static readonly DependencyProperty IsStartPageProperty =
DependencyProperty.RegisterAttached("IsStartPage", typeof(bool),
typeof(NavProperties), new PropertyMetadata(false));
}
NavigationView でこの新しいプロパティを使用すると、Start プロパティが設定された MenuItems 内に NavigationViewItem を配置してから、コントロールが正常に読み込まれたときにそこにナビゲートすることになります。このロジックは省略できます。次に示すように、設定をサポートしますが必須ではありません。
Loaded += (s, e) =>
{
if (FindStart() is NavigationViewItem i && i != null)
Navigate(_frame, i.GetValue(NavProperties.PageTypeProperty) as Type);
};
NavigationViewItem FindStart()
=> MenuItems.OfType<NavigationViewItem>()
.SingleOrDefault(x => (bool)x.GetValue(NavProperties.IsStartPageProperty));
まず、FindStart メソッドで LINQ の SingleOrDefault セレクターが使用されているのがわかります。これはそのセレクターの兄弟とは対照的です。FirstOrDefault は見つかった最初の要素を返しますが、SingleOrDefault はその述語によって複数の要素が検出されると例外をスローします。初期ページは必ず 1 つだけ宣言する必要があるため、これは開発者にプロパティの使用法を示し、使用を促進するのに役立ちます。
ページ ヘッダー図 2 に示すように、NavigationView の Header は省略できません。ヘッダー領域は Page の上部にあり、高さが 48 ピクセルに固定され、表示対象はグローバル コンテンツです。シンプルなタイトルを実装するのは、Header プロパティを Page オブジェクトにアタッチするだけです。以下に例を示します。
public partial class NavProperties : DependencyObject
{
public static string GetHeader(Page obj)
=> (string)obj.GetValue(HeaderProperty);
public static void SetHeader(Page obj, string value)
=> obj.SetValue(HeaderProperty, value);
public static readonly DependencyProperty HeaderProperty =
DependencyProperty.RegisterAttached("Header", typeof(string),
typeof(NavProperties), new PropertyMetadata(null));
}
Frame の Navigated イベントでは、NavViewEx が結果ページでそのプロパティを探して、省略可能な値を NavigationView の Header に挿入します。新しい Page 添付プロパティのスコープは個別のページに設定でき、UWP x:Uid ローカライズ サブシステムを通じてローカライズできます。図 6 のコードは、ヘッダーを効率的に更新する方法として、拡張コントロールに 2 行だけ新しいコードを導入しています。
図 6 ヘッダーの更新
private void Frame_Navigated(object sender,
Windows.UI.Xaml.Navigation.NavigationEventArgs e)
{
SelectedItem = Find(e.SourcePageType);
UpdateHeader();
}
private void UpdateHeader()
{
if (_frame.Content is Page p
&& p.GetValue(NavProperties.HeaderProperty) is string s
&& !string.IsNullOrEmpty(s))
{
Header = s;
}
}
この簡単な例では、Header は既定の TextBlock を受け取ります。筆者の経験と、Microsoft 製ファーストパーティのインボックス アプリによる裏付けによると、CommandBar コントロールは通常、この有用な画面領域を使用します。自作のアプリで同じ目的を果たすには、次のシンプルなマークアップを使って HeaderTemplate プロパティを更新します。
<NavigationView.HeaderTemplate>
<DataTemplate>
<CommandBar>
<CommandBar.Content>
<Grid Margin="12,5,0,11" VerticalAlignment="Stretch">
<TextBlock Text="{Binding}"
Style="{StaticResource TitleTextBlockStyle}"
TextWrapping="NoWrap" VerticalAlignment="Bottom"/>
</Grid>
</CommandBar.Content>
</CommandBar>
</DataTemplate>
</NavigationView.HeaderTemplate>
その TextBlock スタイルはコントロールの既定の Header を模倣して、グローバルに利用可能な CommandBar に配置しています。これはプログラムを使って、アプリでページ単位にまたはグローバルなコンテキストで実装できます。そのため、基本デザインの見た目は同じですが、その機能の可能性は大幅に広がります。
狭い領域での項目ヘッダーの問題
問題が 1 つ残っています。前半で説明したように、NavigationView には、ビューの幅によって変わるさまざまな表示モードが用意されています。また、メニュー ウィンドウを明示的に開いて閉じることもできます。メニュー ウィンドウが開いているとき、その幅は OpenPaneLength プロパティの値によって決まります。余計なお世話かもしれませんが、プロパティ名の末尾は Width ではなく Length です。とにかく重要なのは、そのプロパティ値はメニュー ウィンドウが閉じられたときにその幅に影響しないことです。閉じられた状態では、ウィンドウの幅は 48 ピクセル にハードコーディングされています。
ここで、NavigationViewItems はそのアイコンの幅が 48 ピクセルに設定された状態では美しく表示されますが、NavigationViewItemHeaders には 1 つの Content プロパティしかないので、ウィンドウが開いていても閉じられても同じ表示になります。ウィンドウが開いているときは優れた外観になりますが、ウィンドウが閉じられているときはテキストの一部しか表示されません (図 7 参照)。
図 7 開いている状態と閉じられている (狭い) 状態の NavigationViewHeader
どうすればよいでしょう。まず、ヘッダーにアイコンを追加することを考えましたが、ウィンドウが閉じられると、NavigationViewItem と同様の表示になり、その上、タップに応答しないという奇妙でイライラしかねない動作が生じます。代わりのテキストについても考えましたが、48 ピクセルではわずか 3 文字分の領域しかありません。最終的には、次のコード スニペットに示すように、ウィンドウが閉じられたときはヘッダーを非表示にすることにたどり着きました。
RegisterPropertyChangedCallback(IsPaneOpenProperty, IsPaneOpenChanged);
private void IsPaneOpenChanged(DependencyObject sender,
DependencyProperty dp)
{
foreach (var item in MenuItems.OfType<NavigationViewItemHeader>())
{
item.Opacity = IsPaneOpen ? 1: 0;
}
}
この場合、ヘッダーの可視性を変更することで、リスト内の項目が急に動くのを避けています。これは実装が最も簡単なだけでなく、視覚的にも快適です。また、その理由についてもある程度直観的に理解できます。NavigationView は Opened イベントも Closed イベントも公開しないため、RegisterPropertyChangedCallback (Windows 8 で導入された便利なユーティリティ) を使って IsPaneOpenProperty の依存関係プロパティの変化を登録します。ここでは、コールバックを識別して、すべてのヘッダーを切り替えます。必要に応じて別の方法でさまざまなヘッダーを扱うこともできますが、この例ではすべてのヘッダーの処理が同じになります。
まとめ
ユニバーサル Windows プラットフォームと XAML が優れているのは、問題に対してさまざまな解決策があることです。すべての開発者のニーズを満たすコントロールはありません。すべての設計のニーズを満たす API はありません。開発者に対して大きな愛情を持ってリッチ プラットフォームを構築することで、潜在的な問題が解決策に変わります。必要なのは少量のコードと少しの努力だけです。エコシステム別にアプリを設定して独特の価値を提供する独自のシグネチャ エクスペリエンスを作成できます。ハンバーガー メニューでさえ、外観に簡単に手を加え、あらゆる場所にある拡張機能にアクセスできる機会を提供できるようになります。
Jerry Nixon はコロラド州で執筆活動や講演を行っている開発者兼エバンジェリストです。彼はコードを組み立てて優れたアプリをビルドすることを目指して、世界中の開発者のトレーニングと啓発に取り組んでいます。余暇のほとんどは、スタートレックのキャラクターの経歴やエピソードのプロットを 3 人の娘に教えて過ごしています。
この記事のレビューに協力してくれた技術スタッフの Daren May に心より感謝いたします。
Daren May は 4 年連続で Windows Development MVP を受賞しており、開発者のトレーニングとカスタム開発を行う会社 CustomMayd を経営しています。