From a5fc9ebbe4a05a756a2f504f0b49b0b0ba92d1d4 Mon Sep 17 00:00:00 2001 From: Fabian Sauter Date: Sun, 15 Aug 2021 14:12:34 +0200 Subject: [PATCH 1/2] Windows 10X TwoPaneView for ListDetailsView --- .../Controls/ListDetailsView.Metadata.cs | 12 +- .../Controls/ListDetailsView.Typedata.cs | 9 +- .../ListDetailsView/BackButtonBehavior.cs | 4 +- .../ListDetailsView.BackButton.cs | 166 +++++++ .../ListDetailsView/ListDetailsView.Events.cs | 6 +- .../ListDetailsView.Properties.cs | 185 +++++++- .../ListDetailsView/ListDetailsView.cs | 408 +++++++----------- .../ListDetailsView/ListDetailsView.xaml | 262 +++++------ .../ListDetailsView/ListDetailsViewState.cs | 8 +- .../UI/Controls/Test_ListDetailsView.cs | 94 +++- 10 files changed, 734 insertions(+), 420 deletions(-) create mode 100644 Microsoft.Toolkit.Uwp.UI.Controls.Layout/ListDetailsView/ListDetailsView.BackButton.cs diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Layout.Design/Controls/ListDetailsView.Metadata.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Layout.Design/Controls/ListDetailsView.Metadata.cs index 3013c7fdd2b..45bba363e2f 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Layout.Design/Controls/ListDetailsView.Metadata.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Layout.Design/Controls/ListDetailsView.Metadata.cs @@ -2,12 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.ComponentModel; - using Microsoft.Toolkit.Uwp.UI.Controls.Design.Properties; - using Microsoft.VisualStudio.DesignTools.Extensibility; using Microsoft.VisualStudio.DesignTools.Extensibility.Metadata; +using System.ComponentModel; namespace Microsoft.Toolkit.Uwp.UI.Controls.Design { @@ -25,6 +23,7 @@ public ListDetailsViewMetadata() new EditorBrowsableAttribute(EditorBrowsableState.Advanced) ); b.AddCustomAttributes(nameof(ListDetailsView.ListPaneBackground), new CategoryAttribute(Resources.CategoryBrush)); + b.AddCustomAttributes(nameof(ListDetailsView.DetailsPaneBackground), new CategoryAttribute(Resources.CategoryBrush)); b.AddCustomAttributes(nameof(ListDetailsView.ListHeader), new CategoryAttribute(Resources.CategoryCommon)); b.AddCustomAttributes(nameof(ListDetailsView.ListHeaderTemplate), new CategoryAttribute(Resources.CategoryAppearance), @@ -36,6 +35,11 @@ public ListDetailsViewMetadata() new CategoryAttribute(Resources.CategoryCommon), new EditorBrowsableAttribute(EditorBrowsableState.Advanced) ); + AddCustomAttributes(nameof(ListDetailsView.ListPaneNoItemsContent), new CategoryAttribute(Resources.CategoryCommon)); + b.AddCustomAttributes(nameof(ListDetailsView.ListPaneNoItemsContentTemplate), + new CategoryAttribute(Resources.CategoryCommon), + new EditorBrowsableAttribute(EditorBrowsableState.Advanced) + ); b.AddCustomAttributes(nameof(ListDetailsView.ViewState), new CategoryAttribute(Resources.CategoryCommon)); b.AddCustomAttributes(nameof(ListDetailsView.ListCommandBar), new CategoryAttribute(Resources.CategoryCommon)); b.AddCustomAttributes(nameof(ListDetailsView.DetailsCommandBar), new CategoryAttribute(Resources.CategoryCommon)); @@ -44,4 +48,4 @@ public ListDetailsViewMetadata() ); } } -} \ No newline at end of file +} diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Layout.Design/Controls/ListDetailsView.Typedata.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Layout.Design/Controls/ListDetailsView.Typedata.cs index 15c2bcb7d65..d2472467486 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Layout.Design/Controls/ListDetailsView.Typedata.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Layout.Design/Controls/ListDetailsView.Typedata.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; - namespace Microsoft.Toolkit.Uwp.UI.Controls.Design { internal static partial class ControlTypes @@ -15,6 +13,11 @@ internal static class ListDetailsView { internal const string DetailsCommandBar = nameof(DetailsCommandBar); internal const string DetailsTemplate = nameof(DetailsTemplate); + internal const string DetailsPaneBackground = nameof(DetailsPaneBackground); + internal const string DetailsContentTemplateSelector = nameof(DetailsContentTemplateSelector); + internal const string ListPaneItemTemplateSelector = nameof(ListPaneItemTemplateSelector); + internal const string ListPaneNoItemsContentTemplate = nameof(ListPaneNoItemsContentTemplate); + internal const string ListPaneNoItemsContent = nameof(ListPaneNoItemsContent); internal const string ListCommandBar = nameof(ListCommandBar); internal const string ListHeader = nameof(ListHeader); internal const string ListHeaderTemplate = nameof(ListHeaderTemplate); @@ -25,4 +28,4 @@ internal static class ListDetailsView internal const string SelectedItem = nameof(SelectedItem); internal const string ViewState = nameof(ViewState); } -} \ No newline at end of file +} diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/ListDetailsView/BackButtonBehavior.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/ListDetailsView/BackButtonBehavior.cs index d3c8093d7d2..9fa9ee80ce4 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/ListDetailsView/BackButtonBehavior.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/ListDetailsView/BackButtonBehavior.cs @@ -14,7 +14,7 @@ public enum BackButtonBehavior /// /// /// If the back button controlled by is already visible, the will hook into that button. - /// If the new NavigationView provided by the Windows UI nuget package is used, the will enable and show that button. + /// If the new NavigationView provided by the Windows UI NuGet package is used, the will enable and show that button. /// Otherwise the inline button is used. /// Automatic, @@ -34,4 +34,4 @@ public enum BackButtonBehavior /// Manual, } -} \ No newline at end of file +} diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/ListDetailsView/ListDetailsView.BackButton.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/ListDetailsView/ListDetailsView.BackButton.cs new file mode 100644 index 00000000000..ff046c6781b --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/ListDetailsView/ListDetailsView.BackButton.cs @@ -0,0 +1,166 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Reflection; +using Windows.ApplicationModel; +using Windows.UI.Core; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Navigation; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// Panel that allows for a List/Details pattern. + /// + /// + public partial class ListDetailsView + { + private AppViewBackButtonVisibility? _previousSystemBackButtonVisibility; + private bool _previousNavigationViewBackEnabled; + + // Int used because the underlying type is an enum, but we don't have access to the enum + private int _previousNavigationViewBackVisibilty; + private Button _inlineBackButton; + private object _navigationView; + private Frame _frame; + + /// + /// Sets the back button visibility based on the current visual state and selected item + /// + private void SetBackButtonVisibility(ListDetailsViewState? previousState = null) + { + const int backButtonVisible = 1; + + if (DesignMode.DesignModeEnabled) + { + return; + } + + if (ViewState == ListDetailsViewState.Details) + { + if ((BackButtonBehavior == BackButtonBehavior.Inline) && (_inlineBackButton != null)) + { + _inlineBackButton.Visibility = Visibility.Visible; + } + else if (BackButtonBehavior == BackButtonBehavior.Automatic) + { + // Continue to support the system back button if it is being used + SystemNavigationManager navigationManager = SystemNavigationManager.GetForCurrentView(); + if (navigationManager.AppViewBackButtonVisibility == AppViewBackButtonVisibility.Visible) + { + // Setting this indicates that the system back button is being used + _previousSystemBackButtonVisibility = navigationManager.AppViewBackButtonVisibility; + } + else if ((_inlineBackButton != null) && ((_navigationView == null) || (_frame == null))) + { + // We can only use the new NavigationView if we also have a Frame + // If there is no frame we have to use the inline button + _inlineBackButton.Visibility = Visibility.Visible; + } + else + { + SetNavigationViewBackButtonState(backButtonVisible, true); + } + } + else if (BackButtonBehavior != BackButtonBehavior.Manual) + { + SystemNavigationManager navigationManager = SystemNavigationManager.GetForCurrentView(); + _previousSystemBackButtonVisibility = navigationManager.AppViewBackButtonVisibility; + + navigationManager.AppViewBackButtonVisibility = AppViewBackButtonVisibility.Visible; + } + } + else if (previousState == ListDetailsViewState.Details) + { + if ((BackButtonBehavior == BackButtonBehavior.Inline) && (_inlineBackButton != null)) + { + _inlineBackButton.Visibility = Visibility.Collapsed; + } + else if (BackButtonBehavior == BackButtonBehavior.Automatic) + { + if (!_previousSystemBackButtonVisibility.HasValue) + { + if ((_inlineBackButton != null) && ((_navigationView == null) || (_frame == null))) + { + _inlineBackButton.Visibility = Visibility.Collapsed; + } + else + { + SetNavigationViewBackButtonState(_previousNavigationViewBackVisibilty, _previousNavigationViewBackEnabled); + } + } + } + + if (_previousSystemBackButtonVisibility.HasValue) + { + // Make sure we show the back button if the stack can navigate back + SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility = _previousSystemBackButtonVisibility.Value; + _previousSystemBackButtonVisibility = null; + } + } + } + + private void SetNavigationViewBackButtonState(int visible, bool enabled) + { + if (_navigationView == null) + { + return; + } + + System.Type navType = _navigationView.GetType(); + PropertyInfo visibleProperty = navType.GetProperty("IsBackButtonVisible"); + if (visibleProperty != null) + { + _previousNavigationViewBackVisibilty = (int)visibleProperty.GetValue(_navigationView); + visibleProperty.SetValue(_navigationView, visible); + } + + PropertyInfo enabledProperty = navType.GetProperty("IsBackEnabled"); + if (enabledProperty != null) + { + _previousNavigationViewBackEnabled = (bool)enabledProperty.GetValue(_navigationView); + enabledProperty.SetValue(_navigationView, enabled); + } + } + + /// + /// Closes the details pane if we are in narrow state + /// + /// The sender + /// The event args + private void OnFrameNavigating(object sender, NavigatingCancelEventArgs args) + { + if ((args.NavigationMode == NavigationMode.Back) && (ViewState == ListDetailsViewState.Details)) + { + ClearSelectedItem(); + args.Cancel = true; + } + } + + /// + /// Closes the details pane if we are in narrow state + /// + /// The sender + /// The event args + private void OnBackRequested(object sender, BackRequestedEventArgs args) + { + if (ViewState == ListDetailsViewState.Details) + { + // let the OnFrameNavigating method handle it if + if (_frame == null || !_frame.CanGoBack) + { + ClearSelectedItem(); + } + + args.Handled = true; + } + } + + private void OnInlineBackButtonClicked(object sender, RoutedEventArgs e) + { + ClearSelectedItem(); + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/ListDetailsView/ListDetailsView.Events.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/ListDetailsView/ListDetailsView.Events.cs index ed3009f2ac5..d4ebd8a67cf 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/ListDetailsView/ListDetailsView.Events.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/ListDetailsView/ListDetailsView.Events.cs @@ -10,7 +10,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Controls /// /// Panel that allows for a List/Details pattern. /// - /// + /// public partial class ListDetailsView { /// @@ -19,7 +19,7 @@ public partial class ListDetailsView public event SelectionChangedEventHandler SelectionChanged; /// - /// Occurs when the view state changes + /// Occurs when the view state changes. /// public event EventHandler ViewStateChanged; @@ -28,4 +28,4 @@ private void OnSelectionChanged(SelectionChangedEventArgs e) SelectionChanged?.Invoke(this, e); } } -} \ No newline at end of file +} diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/ListDetailsView/ListDetailsView.Properties.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/ListDetailsView/ListDetailsView.Properties.cs index 1cd9caa3c09..d9c5df75e5d 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/ListDetailsView/ListDetailsView.Properties.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/ListDetailsView/ListDetailsView.Properties.cs @@ -45,6 +45,36 @@ public partial class ListDetailsView typeof(ListDetailsView), new PropertyMetadata(null)); + /// + /// Identifies the dependency property. + /// + /// The identifier for the dependency property. + public static readonly DependencyProperty DetailsContentTemplateSelectorProperty = DependencyProperty.Register( + nameof(DetailsContentTemplateSelector), + typeof(DataTemplateSelector), + typeof(ListDetailsView), + new PropertyMetadata(null)); + + /// + /// Identifies the dependency property. + /// + /// The identifier for the dependency property. + public static readonly DependencyProperty ListPaneItemTemplateSelectorProperty = DependencyProperty.Register( + nameof(ListPaneItemTemplateSelector), + typeof(DataTemplateSelector), + typeof(ListDetailsView), + new PropertyMetadata(null)); + + /// + /// Identifies the dependency property. + /// + /// The identifier for the dependency property. + public static readonly DependencyProperty DetailsPaneBackgroundProperty = DependencyProperty.Register( + nameof(DetailsPaneBackground), + typeof(Brush), + typeof(ListDetailsView), + new PropertyMetadata(null)); + /// /// Identifies the dependency property. /// @@ -75,6 +105,26 @@ public partial class ListDetailsView typeof(ListDetailsView), new PropertyMetadata(null)); + /// + /// Identifies the dependency property. + /// + /// The identifier for the dependency property. + public static readonly DependencyProperty ListPaneNoItemsContentProperty = DependencyProperty.Register( + nameof(ListPaneNoItemsContent), + typeof(object), + typeof(ListDetailsView), + new PropertyMetadata(null)); + + /// + /// Identifies the dependency property. + /// + /// The identifier for the dependency property. + public static readonly DependencyProperty ListPaneNoItemsContentTemplateProperty = DependencyProperty.Register( + nameof(ListPaneNoItemsContentTemplate), + typeof(DataTemplate), + typeof(ListDetailsView), + new PropertyMetadata(null)); + /// /// Identifies the dependency property. /// @@ -103,7 +153,7 @@ public partial class ListDetailsView nameof(ListPaneWidth), typeof(double), typeof(ListDetailsView), - new PropertyMetadata(320d)); + new PropertyMetadata(320d, OnListPaneWidthChanged)); /// /// Identifies the dependency property. @@ -162,7 +212,7 @@ public partial class ListDetailsView nameof(CompactModeThresholdWidth), typeof(double), typeof(ListDetailsView), - new PropertyMetadata(720d, OnCompactModeThresholdWidthChanged)); + new PropertyMetadata(640d)); /// /// Identifies the dependency property @@ -202,6 +252,35 @@ public DataTemplate DetailsTemplate set { SetValue(DetailsTemplateProperty, value); } } + /// + /// Gets or sets the for the details presenter. + /// + public DataTemplateSelector DetailsContentTemplateSelector + { + get { return (DataTemplateSelector)GetValue(DetailsContentTemplateSelectorProperty); } + set { SetValue(DetailsContentTemplateSelectorProperty, value); } + } + + /// + /// Gets or sets the for the list pane items. + /// + public DataTemplateSelector ListPaneItemTemplateSelector + { + get { return (DataTemplateSelector)GetValue(ListPaneItemTemplateSelectorProperty); } + set { SetValue(ListPaneItemTemplateSelectorProperty, value); } + } + + /// + /// Gets or sets the content for the list pane's header + /// Gets or sets the Brush to apply to the background of the details area of the control. + /// + /// The Brush to apply to the background of the details area of the control. + public Brush DetailsPaneBackground + { + get { return (Brush)GetValue(DetailsPaneBackgroundProperty); } + set { SetValue(DetailsPaneBackgroundProperty, value); } + } + /// /// Gets or sets the Brush to apply to the background of the list area of the control. /// @@ -236,6 +315,30 @@ public DataTemplate ListHeaderTemplate set { SetValue(ListHeaderTemplateProperty, value); } } + /// + /// Gets or sets the content for the list pane's no items presenter. + /// + /// + /// The content of the list pane's header. The default is null. + /// + public object ListPaneNoItemsContent + { + get { return GetValue(ListPaneNoItemsContentProperty); } + set { SetValue(ListPaneNoItemsContentProperty, value); } + } + + /// + /// Gets or sets the DataTemplate used to display the list pane's no items presenter. + /// + /// + /// The template that specifies the visualization of the list pane no items object. The default is null. + /// + public DataTemplate ListPaneNoItemsContentTemplate + { + get { return (DataTemplate)GetValue(ListPaneNoItemsContentTemplateProperty); } + set { SetValue(ListPaneNoItemsContentTemplateProperty, value); } + } + /// /// Gets or sets the content for the details pane's header /// @@ -346,5 +449,81 @@ public BackButtonBehavior BackButtonBehavior /// This new model will be the DataContext of the Details area. /// public Func MapDetails { get; set; } + + /// + /// Fired when the DetailsCommandBar changes. + /// + /// The sender. + /// The event args. + private static void OnDetailsCommandBarChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + ((ListDetailsView)d).OnDetailsCommandBarChanged(); + } + + /// + /// Fired when the changes. + /// + /// The sender. + /// The event args. + private static void OnListCommandBarChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + ((ListDetailsView)d).OnListCommandBarChanged(); + } + + /// + /// Fired when the SelectedItem changes. + /// + /// The sender. + /// The event args. + /// + /// Sets up animations for the DetailsPresenter for animating in/out. + /// + private static void OnSelectedItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + ((ListDetailsView)d).OnSelectedItemChanged(e); + } + + /// + /// Fired when the SelectedIndex changes. + /// + /// The sender. + /// The event args. + /// + /// Sets up animations for the DetailsPresenter for animating in/out. + /// + private static void OnSelectedIndexChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + ((ListDetailsView)d).OnSelectedIndexChanged(e); + } + + /// + /// Fired when the is changed. + /// + /// The sender. + /// The event args. + private static void OnBackButtonBehaviorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + ((ListDetailsView)d).SetBackButtonVisibility(); + } + + /// + /// Fired when the is changed. + /// + /// The sender. + /// The event args. + private static void OnListHeaderChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + ((ListDetailsView)d).SetListHeaderVisibility(); + } + + /// + /// Fired when the is changed. + /// + /// The sender. + /// The event args. + private static void OnListPaneWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + ((ListDetailsView)d).OnListPaneWidthChanged(); + } } -} \ No newline at end of file +} diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/ListDetailsView/ListDetailsView.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/ListDetailsView/ListDetailsView.cs index 896a2c75a26..2dbcc0da1c4 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/ListDetailsView/ListDetailsView.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/ListDetailsView/ListDetailsView.cs @@ -3,13 +3,11 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; -using System.Linq; using Windows.ApplicationModel; using Windows.UI.Core; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Input; -using Windows.UI.Xaml.Navigation; using NavigationView = Microsoft.UI.Xaml.Controls; namespace Microsoft.Toolkit.Uwp.UI.Controls @@ -23,32 +21,32 @@ namespace Microsoft.Toolkit.Uwp.UI.Controls [TemplateVisualState(Name = NoSelectionWideState, GroupName = SelectionStates)] [TemplateVisualState(Name = HasSelectionWideState, GroupName = SelectionStates)] [TemplateVisualState(Name = HasSelectionNarrowState, GroupName = SelectionStates)] - [TemplateVisualState(Name = NarrowState, GroupName = WidthStates)] - [TemplateVisualState(Name = WideState, GroupName = WidthStates)] public partial class ListDetailsView : ItemsControl { - private const string PartDetailsPresenter = "DetailsPresenter"; - private const string PartDetailsPanel = "DetailsPanel"; - private const string PartBackButton = "ListDetailsBackButton"; - private const string PartHeaderContentPresenter = "HeaderContentPresenter"; - private const string NarrowState = "NarrowState"; - private const string WideState = "WideState"; - private const string WidthStates = "WidthStates"; + // All view states: private const string SelectionStates = "SelectionStates"; - private const string HasSelectionNarrowState = "HasSelectionNarrow"; + private const string NoSelectionWideState = "NoSelectionWide"; private const string HasSelectionWideState = "HasSelectionWide"; private const string NoSelectionNarrowState = "NoSelectionNarrow"; - private const string NoSelectionWideState = "NoSelectionWide"; + private const string HasSelectionNarrowState = "HasSelectionNarrow"; + + private const string HasItemsStates = "HasItemsStates"; + private const string HasItemsState = "HasItemsState"; + private const string HasNoItemsState = "HasNoItemsState"; - private AppViewBackButtonVisibility? _previousSystemBackButtonVisibility; - private bool _previousNavigationViewBackEnabled; + // Control names: + private const string PartRootPanel = "RootPanel"; + private const string PartDetailsPresenter = "DetailsPresenter"; + private const string PartDetailsPanel = "DetailsPanel"; + private const string PartMasterList = "MasterList"; + private const string PartBackButton = "ListDetailsBackButton"; + private const string PartHeaderContentPresenter = "HeaderContentPresenter"; + private const string PartListPaneCommandBarPanel = "ListPaneCommandBarPanel"; + private const string PartDetailsPaneCommandBarPanel = "DetailsPaneCommandBarPanel"; - private NavigationView.NavigationViewBackButtonVisible _previousNavigationViewBackVisibilty; - private NavigationView.NavigationView _navigationView; private ContentPresenter _detailsPresenter; + private NavigationView.TwoPaneView _twoPaneView; private VisualStateGroup _selectionStateGroup; - private Button _inlineBackButton; - private Frame _frame; /// /// Initializes a new instance of the class. @@ -61,6 +59,14 @@ public ListDetailsView() Unloaded += OnUnloaded; } + /// + /// Clears the and prevent flickering of the UI if only the order of the items changed. + /// + public void ClearSelectedItem() + { + SelectedItem = null; + } + /// /// Invoked whenever application code or internal processes (such as a rebuilding layout pass) call /// ApplyTemplate. In simplest terms, this means the method is called just before a UI element displays @@ -81,15 +87,25 @@ protected override void OnApplyTemplate() _inlineBackButton.Click += OnInlineBackButtonClicked; } + _selectionStateGroup = (VisualStateGroup)GetTemplateChild(SelectionStates); + if (_selectionStateGroup != null) + { + _selectionStateGroup.CurrentStateChanged += OnSelectionStateChanged; + } + + _twoPaneView = (NavigationView.TwoPaneView)GetTemplateChild(PartRootPanel); + if (_twoPaneView != null) + { + _twoPaneView.ModeChanged += OnModeChanged; + } + _detailsPresenter = (ContentPresenter)GetTemplateChild(PartDetailsPresenter); SetDetailsContent(); SetListHeaderVisibility(); OnDetailsCommandBarChanged(); OnListCommandBarChanged(); - - SizeChanged -= ListDetailsView_SizeChanged; - SizeChanged += ListDetailsView_SizeChanged; + OnListPaneWidthChanged(); UpdateView(true); } @@ -97,101 +113,61 @@ protected override void OnApplyTemplate() /// /// Fired when the SelectedIndex changes. /// - /// The sender - /// The event args + /// The event args. /// /// Sets up animations for the DetailsPresenter for animating in/out. /// - private static void OnSelectedIndexChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + private void OnSelectedIndexChanged(DependencyPropertyChangedEventArgs e) { - var view = (ListDetailsView)d; - - var newValue = (int)e.NewValue < 0 ? null : view.Items[(int)e.NewValue]; - var oldValue = e.OldValue == null ? null : view.Items.ElementAtOrDefault((int)e.OldValue); - - // check if selection actually changed - if (view.SelectedItem != newValue) + if (e.NewValue is int newIndex) { - // sync SelectedItem - view.SetValue(SelectedItemProperty, newValue); - view.UpdateSelection(oldValue, newValue); + object newItem = newIndex >= 0 && Items.Count > newIndex ? Items[newIndex] : null; + object oldItem = e.OldValue is int oldIndex && oldIndex >= 0 && Items.Count > oldIndex ? Items[oldIndex] : null; + if (SelectedItem != newItem) + { + if (newItem is null) + { + ClearSelectedItem(); + } + else + { + SetValue(SelectedItemProperty, newItem); + UpdateSelection(oldItem, newItem); + } + } } } /// /// Fired when the SelectedItem changes. /// - /// The sender - /// The event args + /// The event args. /// /// Sets up animations for the DetailsPresenter for animating in/out. /// - private static void OnSelectedItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + private void OnSelectedItemChanged(DependencyPropertyChangedEventArgs e) { - var view = (ListDetailsView)d; - var index = e.NewValue == null ? -1 : view.Items.IndexOf(e.NewValue); + int index = SelectedItem is null ? -1 : Items.IndexOf(SelectedItem); - // check if selection actually changed - if (view.SelectedIndex != index) + // If there is no selection, do not remove the DetailsPresenter content but let it animate out. + if (index >= 0) { - // sync SelectedIndex - view.SetValue(SelectedIndexProperty, index); - view.UpdateSelection(e.OldValue, e.NewValue); + SetDetailsContent(); } - } - - /// - /// Fired when the is changed. - /// - /// The sender - /// The event args - private static void OnListHeaderChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - var view = (ListDetailsView)d; - view.SetListHeaderVisibility(); - } - - /// - /// Fired when the DetailsCommandBar changes. - /// - /// The sender - /// The event args - private static void OnDetailsCommandBarChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - var view = (ListDetailsView)d; - view.OnDetailsCommandBarChanged(); - } - - /// - /// Fired when CompactModeThresholdWIdthChanged - /// - /// The sender - /// The event args - private static void OnCompactModeThresholdWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - ((ListDetailsView)d).HandleStateChanges(); - } - private static void OnBackButtonBehaviorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - var view = (ListDetailsView)d; - view.SetBackButtonVisibility(); - } + if (SelectedIndex != index) + { + SetValue(SelectedIndexProperty, index); + UpdateSelection(e.OldValue, e.NewValue); + } - /// - /// Fired when the changes. - /// - /// The sender - /// The event args - private static void OnListCommandBarChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - var view = (ListDetailsView)d; - view.OnListCommandBarChanged(); + OnSelectionChanged(new SelectionChangedEventArgs(new List { e.OldValue }, new List { e.NewValue })); + UpdateView(true); } private void OnLoaded(object sender, RoutedEventArgs e) { - if (DesignMode.DesignModeEnabled == false) + if (!DesignMode.DesignModeEnabled) { SystemNavigationManager.GetForCurrentView().BackRequested += OnBackRequested; if (_frame != null) @@ -205,14 +181,6 @@ private void OnLoaded(object sender, RoutedEventArgs e) { _frame.Navigating += OnFrameNavigating; } - - _selectionStateGroup = (VisualStateGroup)GetTemplateChild(SelectionStates); - if (_selectionStateGroup != null) - { - _selectionStateGroup.CurrentStateChanged += OnSelectionStateChanged; - } - - UpdateView(true); } } @@ -235,21 +203,6 @@ private void OnUnloaded(object sender, RoutedEventArgs e) } } - private void ListDetailsView_SizeChanged(object sender, SizeChangedEventArgs e) - { - // if size is changing - if ((e.PreviousSize.Width < CompactModeThresholdWidth && e.NewSize.Width >= CompactModeThresholdWidth) || - (e.PreviousSize.Width >= CompactModeThresholdWidth && e.NewSize.Width < CompactModeThresholdWidth)) - { - HandleStateChanges(); - } - } - - private void OnInlineBackButtonClicked(object sender, RoutedEventArgs e) - { - SelectedItem = null; - } - /// /// Raises SelectionChanged event and updates view. /// @@ -268,45 +221,6 @@ private void UpdateSelection(object oldSelection, object newSelection) } } - private void HandleStateChanges() - { - UpdateView(true); - SetListSelectionWithKeyboardFocusOnVisualStateChanged(ViewState); - } - - /// - /// Closes the details pane if we are in narrow state - /// - /// The sender - /// The event args - private void OnFrameNavigating(object sender, NavigatingCancelEventArgs args) - { - if ((args.NavigationMode == NavigationMode.Back) && (ViewState == ListDetailsViewState.Details)) - { - SelectedItem = null; - args.Cancel = true; - } - } - - /// - /// Closes the details pane if we are in narrow state - /// - /// The sender - /// The event args - private void OnBackRequested(object sender, BackRequestedEventArgs args) - { - if (ViewState == ListDetailsViewState.Details) - { - // let the OnFrameNavigating method handle it if - if (_frame == null || !_frame.CanGoBack) - { - SelectedItem = null; - } - - args.Handled = true; - } - } - private void SetListHeaderVisibility() { if (GetTemplateChild(PartHeaderContentPresenter) is FrameworkElement headerPresenter) @@ -323,88 +237,23 @@ private void UpdateView(bool animate) SetVisualState(animate); } - /// - /// Sets the back button visibility based on the current visual state and selected item - /// - private void SetBackButtonVisibility(ListDetailsViewState? previousState = null) + private void UpdateViewState() { - if (DesignMode.DesignModeEnabled) - { - return; - } + ListDetailsViewState previousState = ViewState; - if (ViewState == ListDetailsViewState.Details) + if (_twoPaneView == null) { - if ((BackButtonBehavior == BackButtonBehavior.Inline) && (_inlineBackButton != null)) - { - _inlineBackButton.Visibility = Visibility.Visible; - } - else if (BackButtonBehavior == BackButtonBehavior.Automatic) - { - // Continue to support the system back button if it is being used - var navigationManager = SystemNavigationManager.GetForCurrentView(); - if (navigationManager.AppViewBackButtonVisibility == AppViewBackButtonVisibility.Visible) - { - // Setting this indicates that the system back button is being used - _previousSystemBackButtonVisibility = navigationManager.AppViewBackButtonVisibility; - } - else if ((_inlineBackButton != null) && ((_navigationView == null) || (_frame == null))) - { - // We can only use the new NavigationView if we also have a Frame - // If there is no frame we have to use the inline button - _inlineBackButton.Visibility = Visibility.Visible; - } - else - { - SetNavigationViewBackButtonState(NavigationView.NavigationViewBackButtonVisible.Visible, true); - } - } - else if (BackButtonBehavior != BackButtonBehavior.Manual) - { - var navigationManager = SystemNavigationManager.GetForCurrentView(); - _previousSystemBackButtonVisibility = navigationManager.AppViewBackButtonVisibility; - - navigationManager.AppViewBackButtonVisibility = AppViewBackButtonVisibility.Visible; - } - } - else if (previousState == ListDetailsViewState.Details) - { - if ((BackButtonBehavior == BackButtonBehavior.Inline) && (_inlineBackButton != null)) - { - _inlineBackButton.Visibility = Visibility.Collapsed; - } - else if (BackButtonBehavior == BackButtonBehavior.Automatic) - { - if (_previousSystemBackButtonVisibility.HasValue == false) - { - if ((_inlineBackButton != null) && ((_navigationView == null) || (_frame == null))) - { - _inlineBackButton.Visibility = Visibility.Collapsed; - } - else - { - SetNavigationViewBackButtonState(_previousNavigationViewBackVisibilty, _previousNavigationViewBackEnabled); - } - } - } - - if (_previousSystemBackButtonVisibility.HasValue) - { - // Make sure we show the back button if the stack can navigate back - SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility = _previousSystemBackButtonVisibility.Value; - _previousSystemBackButtonVisibility = null; - } + ViewState = ListDetailsViewState.Both; } - } - - private void UpdateViewState() - { - var previousState = ViewState; - if (ActualWidth < CompactModeThresholdWidth) + // Single pane: + else if (_twoPaneView.Mode == NavigationView.TwoPaneViewMode.SinglePane) { ViewState = SelectedItem == null ? ListDetailsViewState.List : ListDetailsViewState.Details; + _twoPaneView.PanePriority = SelectedItem == null ? NavigationView.TwoPaneViewPriority.Pane1 : NavigationView.TwoPaneViewPriority.Pane2; } + + // Dual pane: else { ViewState = ListDetailsViewState.Both; @@ -419,44 +268,37 @@ private void UpdateViewState() private void SetVisualState(bool animate) { - string state; string noSelectionState; string hasSelectionState; - if (ActualWidth < CompactModeThresholdWidth) - { - state = NarrowState; - noSelectionState = NoSelectionNarrowState; - hasSelectionState = HasSelectionNarrowState; - } - else + if (ViewState == ListDetailsViewState.Both) { - state = WideState; noSelectionState = NoSelectionWideState; hasSelectionState = HasSelectionWideState; } - - VisualStateManager.GoToState(this, state, animate); - VisualStateManager.GoToState(this, SelectedItem == null ? noSelectionState : hasSelectionState, animate); - } - - private void SetNavigationViewBackButtonState(NavigationView.NavigationViewBackButtonVisible visibility, bool enabled) - { - if (_navigationView == null) + else { - return; + noSelectionState = NoSelectionNarrowState; + hasSelectionState = HasSelectionNarrowState; } - _previousNavigationViewBackVisibilty = _navigationView.IsBackButtonVisible; - _navigationView.IsBackButtonVisible = visibility; - - _previousNavigationViewBackEnabled = _navigationView.IsBackEnabled; - _navigationView.IsBackEnabled = enabled; + VisualStateManager.GoToState(this, SelectedItem == null ? noSelectionState : hasSelectionState, animate); + VisualStateManager.GoToState(this, Items.Count > 0 ? HasItemsState : HasNoItemsState, animate); } + /// + /// Sets the content of the based on current function. + /// private void SetDetailsContent() { if (_detailsPresenter != null) { + // Update the content template: + if (_detailsPresenter.ContentTemplateSelector != null) + { + _detailsPresenter.ContentTemplate = _detailsPresenter.ContentTemplateSelector.SelectTemplate(SelectedItem, _detailsPresenter); + } + + // Update the content: _detailsPresenter.Content = MapDetails == null ? SelectedItem : SelectedItem != null ? MapDetails(SelectedItem) : null; @@ -465,12 +307,12 @@ private void SetDetailsContent() private void OnListCommandBarChanged() { - OnCommandBarChanged("ListCommandBarPanel", ListCommandBar); + OnCommandBarChanged("ListCommandBar", ListCommandBar); } private void OnDetailsCommandBarChanged() { - OnCommandBarChanged("DetailsCommandBarPanel", DetailsCommandBar); + OnCommandBarChanged("DetailsCommandBar", DetailsCommandBar); } private void OnCommandBarChanged(string panelName, CommandBar commandbar) @@ -509,7 +351,7 @@ private void SetListSelectionWithKeyboardFocusOnVisualStateChanged(ListDetailsVi /// private void SetListSelectionWithKeyboardFocus(bool singleSelectionFollowsFocus) { - if (GetTemplateChild("List") is Windows.UI.Xaml.Controls.ListViewBase list) + if (GetTemplateChild("List") is ListViewBase list) { list.SingleSelectionFollowsFocus = singleSelectionFollowsFocus; } @@ -563,10 +405,62 @@ private void FocusFirstFocusableElementInDetails() /// private void FocusItemList() { - if (GetTemplateChild("List") is Control list) + if (GetTemplateChild("PartMasterList") is Control list) { list.Focus(FocusState.Programmatic); } } + + /// + /// Fired when the changed its view mode. + /// + /// The sender. + /// The event args. + private void OnModeChanged(NavigationView.TwoPaneView sender, object args) + { + UpdateView(true); + SetListSelectionWithKeyboardFocusOnVisualStateChanged(ViewState); + } + + /// + /// Invoked once the items changed and ensures the visual state is constant. + /// + protected override void OnItemsChanged(object e) + { + base.OnItemsChanged(e); + UpdateView(true); + + if (SelectedIndex < 0) + { + return; + } + + // Ensure we still have the correct index and selected item for the new collection. + // This prevents flickering when the order of the collection changes. + int index = -1; + if (!(Items is null)) + { + index = Items.IndexOf(SelectedItem); + } + + if (index < 0) + { + ClearSelectedItem(); + } + else if (SelectedIndex != index) + { + SetValue(SelectedIndexProperty, index); + } + } + + /// + /// Updates the since it is of type , + /// but is of type . + /// This should be changed in a further release. + /// + private void OnListPaneWidthChanged() + { + _twoPaneView.Pane1Length = new GridLength(ListPaneWidth); + } } -} \ No newline at end of file +} diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/ListDetailsView/ListDetailsView.xaml b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/ListDetailsView/ListDetailsView.xaml index ebc218ada45..3fb14c4d4c1 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/ListDetailsView/ListDetailsView.xaml +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/ListDetailsView/ListDetailsView.xaml @@ -1,6 +1,7 @@ + xmlns:controls="using:Microsoft.Toolkit.Uwp.UI.Controls" + xmlns:muxc="using:Microsoft.UI.Xaml.Controls"> - \ No newline at end of file + diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/ListDetailsView/ListDetailsViewState.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/ListDetailsView/ListDetailsViewState.cs index 217511a5045..9a177605713 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/ListDetailsView/ListDetailsViewState.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/ListDetailsView/ListDetailsViewState.cs @@ -10,18 +10,18 @@ namespace Microsoft.Toolkit.Uwp.UI.Controls public enum ListDetailsViewState { /// - /// Only the List view is shown + /// Only the List view is shown- /// List, /// - /// Only the Details view is shown + /// Only the Details view is shown- /// Details, /// - /// Both the List and Details views are shown + /// Both the List and Details views are shown- /// Both } -} \ No newline at end of file +} diff --git a/UnitTests/UnitTests.UWP/UI/Controls/Test_ListDetailsView.cs b/UnitTests/UnitTests.UWP/UI/Controls/Test_ListDetailsView.cs index 13f1e938c76..c0166b6077f 100644 --- a/UnitTests/UnitTests.UWP/UI/Controls/Test_ListDetailsView.cs +++ b/UnitTests/UnitTests.UWP/UI/Controls/Test_ListDetailsView.cs @@ -5,9 +5,10 @@ using Microsoft.Toolkit.Uwp.UI.Controls; using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer; +using System.Collections.ObjectModel; using System.Linq; -namespace UnitTests.UWP.UI.Controls +namespace UnitTests.UI.Controls { [TestClass] public class Test_ListDetailsView @@ -17,8 +18,10 @@ public class Test_ListDetailsView public void Test_SelectedIndex_Default() { var items = Enumerable.Range(1, 10).ToArray(); - var listDetailsView = new ListDetailsView(); - listDetailsView.ItemsSource = items; + var listDetailsView = new ListDetailsView + { + ItemsSource = items + }; Assert.AreEqual(-1, listDetailsView.SelectedIndex); } @@ -27,8 +30,10 @@ public void Test_SelectedIndex_Default() public void Test_SelectedItem_Default() { var items = Enumerable.Range(1, 10).ToArray(); - var listDetailsView = new ListDetailsView(); - listDetailsView.ItemsSource = items; + var listDetailsView = new ListDetailsView + { + ItemsSource = items + }; Assert.IsNull(listDetailsView.SelectedItem); } @@ -37,9 +42,11 @@ public void Test_SelectedItem_Default() public void Test_SelectedIndex_Syncs_SelectedItem() { var items = Enumerable.Range(1, 10).ToArray(); - var listDetailsView = new ListDetailsView(); - listDetailsView.ItemsSource = items; - listDetailsView.SelectedIndex = 6; + var listDetailsView = new ListDetailsView + { + ItemsSource = items, + SelectedIndex = 6 + }; Assert.AreEqual(items[6], listDetailsView.SelectedItem); } @@ -48,9 +55,11 @@ public void Test_SelectedIndex_Syncs_SelectedItem() public void Test_UnselectUsingIndex() { var items = Enumerable.Range(1, 10).ToArray(); - var listDetailsView = new ListDetailsView(); - listDetailsView.ItemsSource = items; - listDetailsView.SelectedIndex = 5; + var listDetailsView = new ListDetailsView + { + ItemsSource = items, + SelectedIndex = 5 + }; listDetailsView.SelectedIndex = -1; Assert.IsNull(listDetailsView.SelectedItem); } @@ -60,9 +69,11 @@ public void Test_UnselectUsingIndex() public void Test_UnselectUsingItem() { var items = Enumerable.Range(1, 10).ToArray(); - var listDetailsView = new ListDetailsView(); - listDetailsView.ItemsSource = items; - listDetailsView.SelectedItem = items[5]; + var listDetailsView = new ListDetailsView + { + ItemsSource = items, + SelectedItem = items[5] + }; listDetailsView.SelectedItem = null; Assert.AreEqual(-1, listDetailsView.SelectedIndex); } @@ -72,10 +83,57 @@ public void Test_UnselectUsingItem() public void Test_SelectedItem_Syncs_SelectedIndex() { var items = Enumerable.Range(0, 10).ToArray(); - var listDetailsView = new ListDetailsView(); - listDetailsView.ItemsSource = items; - listDetailsView.SelectedItem = items[3]; + var listDetailsView = new ListDetailsView + { + ItemsSource = items, + SelectedItem = items[3] + }; Assert.AreEqual(3, listDetailsView.SelectedIndex); } + + [TestCategory("ListDetailsView")] + [UITestMethod] + public void Test_Sorting_Keeps_SelectedIndex() + { + var items = Enumerable.Range(0, 10).ToArray(); + var listDetailsView = new ListDetailsView + { + ItemsSource = items, + SelectedItem = items[3] + }; + Assert.AreEqual(3, listDetailsView.SelectedIndex); + } + + [TestCategory("ListDetailsView")] + [UITestMethod] + public void Test_Sorting_Keeps_SelectedItem() + { + var items = new ObservableCollection(Enumerable.Range(0, 10)); + var listDetailsView = new ListDetailsView + { + ItemsSource = items, + SelectedIndex = 3 + }; + var item = listDetailsView.SelectedItem; + listDetailsView.ItemsSource = new ObservableCollection(items.OrderByDescending(i => i)); + Assert.AreEqual(item, listDetailsView.SelectedItem); + listDetailsView.ItemsSource = new ObservableCollection(items.OrderBy(i => i)); + Assert.AreEqual(item, listDetailsView.SelectedItem); + } + + [TestCategory("ListDetailsView")] + [UITestMethod] + public void Test_ItemsRemoved() + { + var items = new ObservableCollection(Enumerable.Range(0, 10)); + var listDetailsView = new ListDetailsView + { + ItemsSource = items, + SelectedIndex = 3 + }; + listDetailsView.ItemsSource = null; + Assert.AreEqual(null, listDetailsView.SelectedItem); + Assert.AreEqual(-1, listDetailsView.SelectedIndex); + } } -} \ No newline at end of file +} From e3f5a7f60b24f7caed6fb7945fe4df7ef00ee0c7 Mon Sep 17 00:00:00 2001 From: Fabian Sauter Date: Sun, 15 Aug 2021 14:43:25 +0200 Subject: [PATCH 2/2] Fixed ambiguous doc hint --- .../ListDetailsView/ListDetailsView.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/ListDetailsView/ListDetailsView.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/ListDetailsView/ListDetailsView.cs index 2dbcc0da1c4..0fbefdeceb3 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/ListDetailsView/ListDetailsView.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/ListDetailsView/ListDetailsView.cs @@ -454,7 +454,7 @@ protected override void OnItemsChanged(object e) } /// - /// Updates the since it is of type , + /// Updates the since it is of type 'GridLength', /// but is of type . /// This should be changed in a further release. ///