CmdPal: Fix grid views (#43991)

## Summary of the Pull Request

This PR fixes the crash due to binding to a trimmed property. For this
it converts runtime bindings on GridView to use `{x:Bind}` so this issue
can't happen in the future.

- Fixes a crash related to the `Visibility` property in gallery/grid
views when trimmed during AOT builds.
- Fixes ShowTitle and ShowSubtitle properties, they are now taken into
account in a view.
- Improves UI layout, removes some margins and maches the corner radius
of the item contaienr with the item content in the gallery view.
- Refactores gallery and grid views to move logic from the view to the
view model so we can x:Bind to them.
- Replaces `{Binding}` with `{x:Bind}` to improve performance and enable
compile-time binding validation.
- Properties related to grids are splatted on to the common
`IGridPropertiesViewModel` interface. Subclassing would add extra
overhead without substential benefit.
- Adds new samples to showcase various grid view configurations.

## Pictures? Pictures!

A) Gallery view (with title and subtitle)
<img width="909" height="583" alt="image"
src="https://github.com/user-attachments/assets/b807e7a8-412f-4817-8121-e3470c49e0c0"
/>

B) Gallery view (only title)
<img width="903" height="582" alt="image"
src="https://github.com/user-attachments/assets/b619d63f-04d0-42f2-9207-de256dc5e481"
/>

C) Gallery view (no title or subtitle)
<img width="900" height="583" alt="image"
src="https://github.com/user-attachments/assets/c48cd1fc-8f51-40c1-8bce-607916e9f742"
/>

D) Small icons
<img width="907" height="582" alt="image"
src="https://github.com/user-attachments/assets/8327da0a-fa45-443f-b52c-f0f1edd7b861"
/>

E) Medium icons (with labels)
<img width="914" height="588" alt="image"
src="https://github.com/user-attachments/assets/dee9fab1-54e8-45f8-96d7-502b121a6ac2"
/>

F) Medium icons (no labels)
<img width="915" height="588" alt="image"
src="https://github.com/user-attachments/assets/a32e8af2-6cb1-4106-91db-ca396253c0a3"
/>


<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #43973
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
This commit is contained in:
Jiří Polášek
2025-12-01 02:32:30 +01:00
committed by GitHub
parent 1ba5a258e9
commit 4d3c223402
13 changed files with 404 additions and 115 deletions

View File

@@ -11,15 +11,15 @@ public class GalleryGridPropertiesViewModel : IGridPropertiesViewModel
{
private readonly ExtensionObject<IGalleryGridLayout> _model;
public bool ShowTitle { get; private set; }
public bool ShowSubtitle { get; private set; }
public GalleryGridPropertiesViewModel(IGalleryGridLayout galleryGridLayout)
{
_model = new(galleryGridLayout);
}
public bool ShowTitle { get; set; }
public bool ShowSubtitle { get; set; }
public void InitializeProperties()
{
var model = _model.Unsafe;

View File

@@ -6,5 +6,9 @@ namespace Microsoft.CmdPal.Core.ViewModels;
public interface IGridPropertiesViewModel
{
bool ShowTitle { get; }
bool ShowSubtitle { get; }
void InitializeProperties();
}

View File

@@ -10,10 +10,9 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Core.ViewModels;
public partial class ListItemViewModel(IListItem model, WeakReference<IPageContext> context)
: CommandItemViewModel(new(model), context)
public partial class ListItemViewModel : CommandItemViewModel
{
public new ExtensionObject<IListItem> Model { get; } = new(model);
public new ExtensionObject<IListItem> Model { get; }
public List<TagViewModel>? Tags { get; set; }
@@ -32,6 +31,40 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
public string AccessibleName { get; private set; } = string.Empty;
public bool ShowTitle { get; private set; }
public bool ShowSubtitle { get; private set; }
public bool LayoutShowsTitle
{
get;
set
{
if (SetProperty(ref field, value))
{
UpdateShowsTitle();
}
}
}
public bool LayoutShowsSubtitle
{
get;
set
{
if (SetProperty(ref field, value))
{
UpdateShowsSubtitle();
}
}
}
public ListItemViewModel(IListItem model, WeakReference<IPageContext> context)
: base(new(model), context)
{
Model = new ExtensionObject<IListItem>(model);
}
public override void InitializeProperties()
{
if (IsInitialized)
@@ -93,16 +126,18 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
switch (propertyName)
{
case nameof(Tags):
case nameof(model.Tags):
UpdateTags(model.Tags);
break;
case nameof(TextToSuggest):
this.TextToSuggest = model.TextToSuggest ?? string.Empty;
case nameof(model.TextToSuggest):
TextToSuggest = model.TextToSuggest ?? string.Empty;
UpdateProperty(nameof(TextToSuggest));
break;
case nameof(Section):
this.Section = model.Section ?? string.Empty;
case nameof(model.Section):
Section = model.Section ?? string.Empty;
UpdateProperty(nameof(Section));
break;
case nameof(Details):
case nameof(model.Details):
var extensionDetails = model.Details;
Details = extensionDetails is not null ? new(extensionDetails, PageContext) : null;
Details?.InitializeProperties();
@@ -110,16 +145,24 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
UpdateProperty(nameof(HasDetails));
UpdateShowDetailsCommand();
break;
case nameof(MoreCommands):
case nameof(model.MoreCommands):
UpdateProperty(nameof(MoreCommands));
AddShowDetailsCommands();
break;
case nameof(Title):
case nameof(Subtitle):
case nameof(model.Title):
UpdateProperty(nameof(Title));
UpdateShowsTitle();
UpdateAccessibleName();
break;
case nameof(model.Subtitle):
UpdateProperty(nameof(Subtitle));
UpdateShowsSubtitle();
UpdateAccessibleName();
break;
default:
UpdateProperty(propertyName);
break;
}
UpdateProperty(propertyName);
}
// TODO: Do we want filters to match descriptions and other properties? Tags, etc... Yes?
@@ -206,11 +249,32 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
// many COM exception issues.
Tags = [.. newTags];
UpdateProperty(nameof(Tags));
UpdateProperty(nameof(HasTags));
// We're already in UI thread, so just raise the events
OnPropertyChanged(nameof(Tags));
OnPropertyChanged(nameof(HasTags));
});
}
private void UpdateShowsTitle()
{
var oldShowTitle = ShowTitle;
ShowTitle = LayoutShowsTitle;
if (oldShowTitle != ShowTitle)
{
UpdateProperty(nameof(ShowTitle));
}
}
private void UpdateShowsSubtitle()
{
var oldShowSubtitle = ShowSubtitle;
ShowSubtitle = LayoutShowsSubtitle && !string.IsNullOrWhiteSpace(Subtitle);
if (oldShowSubtitle != ShowSubtitle)
{
UpdateProperty(nameof(ShowSubtitle));
}
}
protected override void UnsafeCleanup()
{
base.UnsafeCleanup();

View File

@@ -24,8 +24,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
// Observable from MVVM Toolkit will auto create public properties that use INotifyPropertyChange change
// https://learn.microsoft.com/dotnet/communitytoolkit/mvvm/observablegroupedcollections for grouping support
[ObservableProperty]
public partial ObservableCollection<ListItemViewModel> FilteredItems { get; set; } = [];
public ObservableCollection<ListItemViewModel> FilteredItems { get; } = [];
public FiltersViewModel? Filters { get; set; }
@@ -224,6 +223,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
// TODO we can probably further optimize this by also keeping a
// HashSet of every ExtensionObject we currently have, and only
// building new viewmodels for the ones we haven't already built.
var showsTitle = GridProperties?.ShowTitle ?? true;
var showsSubtitle = GridProperties?.ShowSubtitle ?? true;
foreach (var item in newItems)
{
// Check for cancellation during item processing
@@ -237,6 +238,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
// If an item fails to load, silently ignore it.
if (viewModel.SafeFastInit())
{
viewModel.LayoutShowsTitle = showsTitle;
viewModel.LayoutShowsSubtitle = showsSubtitle;
newViewModels.Add(viewModel);
}
}
@@ -583,6 +586,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
GridProperties = LoadGridPropertiesViewModel(model.GridProperties);
GridProperties?.InitializeProperties();
UpdateProperty(nameof(GridProperties));
ApplyLayoutToItems();
ShowDetails = model.ShowDetails;
UpdateProperty(nameof(ShowDetails));
@@ -608,22 +612,15 @@ public partial class ListViewModel : PageViewModel, IDisposable
model.ItemsChanged += Model_ItemsChanged;
}
private IGridPropertiesViewModel? LoadGridPropertiesViewModel(IGridProperties? gridProperties)
private static IGridPropertiesViewModel? LoadGridPropertiesViewModel(IGridProperties? gridProperties)
{
if (gridProperties is IMediumGridLayout mediumGridLayout)
return gridProperties switch
{
return new MediumGridPropertiesViewModel(mediumGridLayout);
}
else if (gridProperties is IGalleryGridLayout galleryGridLayout)
{
return new GalleryGridPropertiesViewModel(galleryGridLayout);
}
else if (gridProperties is ISmallGridLayout smallGridLayout)
{
return new SmallGridPropertiesViewModel(smallGridLayout);
}
return null;
IMediumGridLayout mediumGridLayout => new MediumGridPropertiesViewModel(mediumGridLayout),
IGalleryGridLayout galleryGridLayout => new GalleryGridPropertiesViewModel(galleryGridLayout),
ISmallGridLayout smallGridLayout => new SmallGridPropertiesViewModel(smallGridLayout),
_ => null,
};
}
public void LoadMoreIfNeeded()
@@ -685,6 +682,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
GridProperties = LoadGridPropertiesViewModel(model.GridProperties);
GridProperties?.InitializeProperties();
UpdateProperty(nameof(IsGridView));
ApplyLayoutToItems();
break;
case nameof(ShowDetails):
ShowDetails = model.ShowDetails;
@@ -730,6 +728,21 @@ public partial class ListViewModel : PageViewModel, IDisposable
});
}
private void ApplyLayoutToItems()
{
lock (_listLock)
{
var showsTitle = GridProperties?.ShowTitle ?? true;
var showsSubtitle = GridProperties?.ShowSubtitle ?? true;
foreach (var item in Items)
{
item.LayoutShowsTitle = showsTitle;
item.LayoutShowsSubtitle = showsSubtitle;
}
}
}
public void Dispose()
{
GC.SuppressFinalize(this);

View File

@@ -11,13 +11,15 @@ public class MediumGridPropertiesViewModel : IGridPropertiesViewModel
{
private readonly ExtensionObject<IMediumGridLayout> _model;
public bool ShowTitle { get; private set; }
public bool ShowSubtitle => false;
public MediumGridPropertiesViewModel(IMediumGridLayout mediumGridLayout)
{
_model = new(mediumGridLayout);
}
public bool ShowTitle { get; set; }
public void InitializeProperties()
{
var model = _model.Unsafe;

View File

@@ -11,6 +11,10 @@ public class SmallGridPropertiesViewModel : IGridPropertiesViewModel
{
private readonly ExtensionObject<ISmallGridLayout> _model;
public bool ShowTitle => false;
public bool ShowSubtitle => false;
public SmallGridPropertiesViewModel(ISmallGridLayout smallGridLayout)
{
_model = new(smallGridLayout);

View File

@@ -0,0 +1,31 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Microsoft.CmdPal.UI;
internal sealed partial class GridItemContainerStyleSelector : StyleSelector
{
public IGridPropertiesViewModel? GridProperties { get; set; }
public Style? Small { get; set; }
public Style? Medium { get; set; }
public Style? Gallery { get; set; }
protected override Style? SelectStyleCore(object item, DependencyObject container)
{
return GridProperties switch
{
SmallGridPropertiesViewModel => Small,
MediumGridPropertiesViewModel => Medium,
GalleryGridPropertiesViewModel => Gallery,
_ => Medium,
};
}
}

View File

@@ -20,21 +20,12 @@ internal sealed partial class GridItemTemplateSelector : DataTemplateSelector
protected override DataTemplate? SelectTemplateCore(object item, DependencyObject dependencyObject)
{
DataTemplate? dataTemplate = Medium;
if (GridProperties is SmallGridPropertiesViewModel)
return GridProperties switch
{
dataTemplate = Small;
}
else if (GridProperties is MediumGridPropertiesViewModel)
{
dataTemplate = Medium;
}
else if (GridProperties is GalleryGridPropertiesViewModel)
{
dataTemplate = Gallery;
}
return dataTemplate;
SmallGridPropertiesViewModel => Small,
MediumGridPropertiesViewModel => Medium,
GalleryGridPropertiesViewModel => Gallery,
_ => Medium,
};
}
}

View File

@@ -5,33 +5,151 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cmdpalUI="using:Microsoft.CmdPal.UI"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:coreViewModels="using:Microsoft.CmdPal.Core.ViewModels"
xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:help="using:Microsoft.CmdPal.UI.Helpers"
xmlns:local="using:Microsoft.CmdPal.UI"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:CommunityToolkit.WinUI"
xmlns:viewModels="using:Microsoft.CmdPal.UI.ViewModels"
x:Name="PageRoot"
Background="Transparent"
DataContext="{x:Bind ViewModel, Mode=OneWay}"
mc:Ignorable="d">
<Page.Resources>
<!-- TODO: Figure out what we want to do here for filtering/grouping and where -->
<!-- https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.data.collectionviewsource -->
<!--<CollectionViewSource
x:Name="ItemsCVS"
IsSourceGrouped="True"
Source="{x:Bind ViewModel.Items, Mode=OneWay}" />-->
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<converters:StringVisibilityConverter
x:Key="StringVisibilityConverter"
EmptyValue="Collapsed"
NotEmptyValue="Visible" />
<!--
GridViewItemCornerRadius is the corner radius defined in GridView template; make
it bigger to match the radii of the gallery
-->
<CornerRadius x:Key="GalleryGridViewItemContainerCornerRadius">6</CornerRadius>
<CornerRadius x:Key="IconGridViewItemContainerCornerRadius">4</CornerRadius>
<CornerRadius x:Key="GalleryGridViewItemRadius">4</CornerRadius>
<CornerRadius x:Key="SmallGridViewItemCornerRadius">8</CornerRadius>
<CornerRadius x:Key="MediumGridViewItemCornerRadius">8</CornerRadius>
<Style x:Key="IconGridViewItemStyle" TargetType="GridViewItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="GridViewItem">
<ListViewItemPresenter
x:Name="Root"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
CheckBoxBorderBrush="{ThemeResource GridViewItemCheckBoxBorderBrush}"
CheckBoxBrush="{ThemeResource GridViewItemCheckBoxBrush}"
CheckBoxCornerRadius="{ThemeResource GridViewItemCheckBoxCornerRadius}"
CheckBoxDisabledBorderBrush="{ThemeResource GridViewItemCheckBoxDisabledBorderBrush}"
CheckBoxDisabledBrush="{ThemeResource GridViewItemCheckBoxDisabledBrush}"
CheckBoxPointerOverBorderBrush="{ThemeResource GridViewItemCheckBoxPointerOverBorderBrush}"
CheckBoxPointerOverBrush="{ThemeResource GridViewItemCheckBoxPointerOverBrush}"
CheckBoxPressedBorderBrush="{ThemeResource GridViewItemCheckBoxPressedBorderBrush}"
CheckBoxPressedBrush="{ThemeResource GridViewItemCheckBoxPressedBrush}"
CheckBoxSelectedBrush="{ThemeResource GridViewItemCheckBoxSelectedBrush}"
CheckBoxSelectedDisabledBrush="{ThemeResource GridViewItemCheckBoxSelectedDisabledBrush}"
CheckBoxSelectedPointerOverBrush="{ThemeResource GridViewItemCheckBoxSelectedPointerOverBrush}"
CheckBoxSelectedPressedBrush="{ThemeResource GridViewItemCheckBoxSelectedPressedBrush}"
CheckBrush="{ThemeResource GridViewItemCheckBrush}"
CheckDisabledBrush="{ThemeResource GridViewItemCheckDisabledBrush}"
CheckMode="{ThemeResource GridViewItemCheckMode}"
CheckPressedBrush="{ThemeResource GridViewItemCheckPressedBrush}"
ContentMargin="{TemplateBinding Padding}"
ContentTransitions="{TemplateBinding ContentTransitions}"
Control.IsTemplateFocusTarget="True"
CornerRadius="{StaticResource IconGridViewItemContainerCornerRadius}"
DisabledOpacity="{ThemeResource ListViewItemDisabledThemeOpacity}"
DragBackground="{ThemeResource GridViewItemDragBackground}"
DragForeground="{ThemeResource GridViewItemDragForeground}"
DragOpacity="{ThemeResource ListViewItemDragThemeOpacity}"
FocusBorderBrush="{ThemeResource GridViewItemFocusBorderBrush}"
FocusVisualMargin="{TemplateBinding FocusVisualMargin}"
FocusVisualPrimaryBrush="{TemplateBinding FocusVisualPrimaryBrush}"
FocusVisualPrimaryThickness="{TemplateBinding FocusVisualPrimaryThickness}"
FocusVisualSecondaryBrush="{TemplateBinding FocusVisualSecondaryBrush}"
FocusVisualSecondaryThickness="{TemplateBinding FocusVisualSecondaryThickness}"
PlaceholderBackground="{ThemeResource GridViewItemPlaceholderBackground}"
PointerOverBackground="{ThemeResource GridViewItemBackgroundPointerOver}"
PointerOverBorderBrush="{ThemeResource GridViewItemPointerOverBorderBrush}"
PointerOverForeground="{ThemeResource GridViewItemForegroundPointerOver}"
PressedBackground="{ThemeResource GridViewItemBackgroundPressed}"
ReorderHintOffset="{ThemeResource GridViewItemReorderHintThemeOffset}"
SelectedBackground="{ThemeResource GridViewItemBackgroundSelected}"
SelectedBorderBrush="{ThemeResource GridViewItemSelectedBorderBrush}"
SelectedBorderThickness="{ThemeResource GridViewItemSelectedBorderThickness}"
SelectedDisabledBackground="{ThemeResource GridViewItemBackgroundSelectedDisabled}"
SelectedDisabledBorderBrush="{ThemeResource GridViewItemSelectedDisabledBorderBrush}"
SelectedForeground="{ThemeResource GridViewItemForegroundSelected}"
SelectedInnerBorderBrush="{ThemeResource GridViewItemSelectedInnerBorderBrush}"
SelectedPointerOverBackground="{ThemeResource GridViewItemBackgroundSelectedPointerOver}"
SelectedPointerOverBorderBrush="{ThemeResource GridViewItemSelectedPointerOverBorderBrush}"
SelectedPressedBackground="{ThemeResource GridViewItemBackgroundSelectedPressed}"
SelectedPressedBorderBrush="{ThemeResource GridViewItemSelectedPressedBorderBrush}"
SelectionCheckMarkVisualEnabled="{ThemeResource GridViewItemSelectionCheckMarkVisualEnabled}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="GalleryGridViewItemStyle" TargetType="GridViewItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="GridViewItem">
<ListViewItemPresenter
x:Name="Root"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
CheckBoxBorderBrush="{ThemeResource GridViewItemCheckBoxBorderBrush}"
CheckBoxBrush="{ThemeResource GridViewItemCheckBoxBrush}"
CheckBoxCornerRadius="{ThemeResource GridViewItemCheckBoxCornerRadius}"
CheckBoxDisabledBorderBrush="{ThemeResource GridViewItemCheckBoxDisabledBorderBrush}"
CheckBoxDisabledBrush="{ThemeResource GridViewItemCheckBoxDisabledBrush}"
CheckBoxPointerOverBorderBrush="{ThemeResource GridViewItemCheckBoxPointerOverBorderBrush}"
CheckBoxPointerOverBrush="{ThemeResource GridViewItemCheckBoxPointerOverBrush}"
CheckBoxPressedBorderBrush="{ThemeResource GridViewItemCheckBoxPressedBorderBrush}"
CheckBoxPressedBrush="{ThemeResource GridViewItemCheckBoxPressedBrush}"
CheckBoxSelectedBrush="{ThemeResource GridViewItemCheckBoxSelectedBrush}"
CheckBoxSelectedDisabledBrush="{ThemeResource GridViewItemCheckBoxSelectedDisabledBrush}"
CheckBoxSelectedPointerOverBrush="{ThemeResource GridViewItemCheckBoxSelectedPointerOverBrush}"
CheckBoxSelectedPressedBrush="{ThemeResource GridViewItemCheckBoxSelectedPressedBrush}"
CheckBrush="{ThemeResource GridViewItemCheckBrush}"
CheckDisabledBrush="{ThemeResource GridViewItemCheckDisabledBrush}"
CheckMode="{ThemeResource GridViewItemCheckMode}"
CheckPressedBrush="{ThemeResource GridViewItemCheckPressedBrush}"
ContentMargin="{TemplateBinding Padding}"
ContentTransitions="{TemplateBinding ContentTransitions}"
Control.IsTemplateFocusTarget="True"
CornerRadius="{StaticResource GalleryGridViewItemContainerCornerRadius}"
DisabledOpacity="{ThemeResource ListViewItemDisabledThemeOpacity}"
DragBackground="{ThemeResource GridViewItemDragBackground}"
DragForeground="{ThemeResource GridViewItemDragForeground}"
DragOpacity="{ThemeResource ListViewItemDragThemeOpacity}"
FocusBorderBrush="{ThemeResource GridViewItemFocusBorderBrush}"
FocusVisualMargin="{TemplateBinding FocusVisualMargin}"
FocusVisualPrimaryBrush="{TemplateBinding FocusVisualPrimaryBrush}"
FocusVisualPrimaryThickness="{TemplateBinding FocusVisualPrimaryThickness}"
FocusVisualSecondaryBrush="{TemplateBinding FocusVisualSecondaryBrush}"
FocusVisualSecondaryThickness="{TemplateBinding FocusVisualSecondaryThickness}"
PlaceholderBackground="{ThemeResource GridViewItemPlaceholderBackground}"
PointerOverBackground="{ThemeResource GridViewItemBackgroundPointerOver}"
PointerOverBorderBrush="{ThemeResource GridViewItemPointerOverBorderBrush}"
PointerOverForeground="{ThemeResource GridViewItemForegroundPointerOver}"
PressedBackground="{ThemeResource GridViewItemBackgroundPressed}"
ReorderHintOffset="{ThemeResource GridViewItemReorderHintThemeOffset}"
SelectedBackground="{ThemeResource GridViewItemBackgroundSelected}"
SelectedBorderBrush="{ThemeResource GridViewItemSelectedBorderBrush}"
SelectedBorderThickness="{ThemeResource GridViewItemSelectedBorderThickness}"
SelectedDisabledBackground="{ThemeResource GridViewItemBackgroundSelectedDisabled}"
SelectedDisabledBorderBrush="{ThemeResource GridViewItemSelectedDisabledBorderBrush}"
SelectedForeground="{ThemeResource GridViewItemForegroundSelected}"
SelectedInnerBorderBrush="{ThemeResource GridViewItemSelectedInnerBorderBrush}"
SelectedPointerOverBackground="{ThemeResource GridViewItemBackgroundSelectedPointerOver}"
SelectedPointerOverBorderBrush="{ThemeResource GridViewItemSelectedPointerOverBorderBrush}"
SelectedPressedBackground="{ThemeResource GridViewItemBackgroundSelectedPressed}"
SelectedPressedBorderBrush="{ThemeResource GridViewItemSelectedPressedBorderBrush}"
SelectionCheckMarkVisualEnabled="{ThemeResource GridViewItemSelectionCheckMarkVisualEnabled}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<DataTemplate x:Key="TagTemplate" x:DataType="coreViewModels:TagViewModel">
<cpcontrols:Tag
@@ -48,10 +166,17 @@
x:Key="GridItemTemplateSelector"
x:DataType="coreViewModels:ListItemViewModel"
Gallery="{StaticResource GalleryGridItemViewModelTemplate}"
GridProperties="{x:Bind ViewModel.GridProperties}"
GridProperties="{x:Bind ViewModel.GridProperties, Mode=OneWay}"
Medium="{StaticResource MediumGridItemViewModelTemplate}"
Small="{StaticResource SmallGridItemViewModelTemplate}" />
<cmdpalUI:GridItemContainerStyleSelector
x:Key="GridItemContainerStyleSelector"
Gallery="{StaticResource GalleryGridViewItemStyle}"
GridProperties="{x:Bind ViewModel.GridProperties, Mode=OneWay}"
Medium="{StaticResource IconGridViewItemStyle}"
Small="{StaticResource IconGridViewItemStyle}" />
<!-- https://learn.microsoft.com/windows/apps/design/controls/itemsview#specify-the-look-of-the-items -->
<DataTemplate x:Key="ListItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
<Grid
@@ -94,7 +219,7 @@
Text="{x:Bind Subtitle, Mode=OneWay}"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap"
Visibility="{x:Bind Subtitle, Mode=OneWay, Converter={StaticResource StringVisibilityConverter}}" />
Visibility="{x:Bind ShowSubtitle, Mode=OneWay}" />
</StackPanel>
<ItemsControl
@@ -124,11 +249,11 @@
Padding="8,16"
HorizontalAlignment="Center"
VerticalAlignment="Center"
AutomationProperties.Name="{x:Bind Title}"
AutomationProperties.Name="{x:Bind Title, Mode=OneWay}"
BorderThickness="0"
CornerRadius="8"
CornerRadius="{StaticResource SmallGridViewItemCornerRadius}"
Orientation="Vertical"
ToolTipService.ToolTip="{x:Bind Title}">
ToolTipService.ToolTip="{x:Bind Title, Mode=OneWay}">
<cpcontrols:IconBox
x:Name="GridIconBorder"
@@ -145,23 +270,22 @@
</DataTemplate>
<DataTemplate x:Key="MediumGridItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
<StackPanel
<Grid
Width="100"
Height="100"
Padding="8,16"
HorizontalAlignment="Center"
VerticalAlignment="Center"
AutomationProperties.Name="{x:Bind Title}"
BorderThickness="0"
CornerRadius="8"
Orientation="Vertical"
ToolTipService.ToolTip="{x:Bind Title}">
Padding="8"
AutomationProperties.Name="{x:Bind Title, Mode=OneWay}"
CornerRadius="{StaticResource MediumGridViewItemCornerRadius}"
ToolTipService.ToolTip="{x:Bind Title, Mode=OneWay}">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<cpcontrols:IconBox
x:Name="GridIconBorder"
Grid.Row="0"
Width="36"
Height="36"
Margin="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
CharacterSpacing="12"
@@ -169,21 +293,20 @@
Foreground="{ThemeResource TextFillColorPrimary}"
SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
<TextBlock
x:Name="TitleTextBlock"
MaxHeight="40"
Margin="0,8,0,4"
Grid.Row="1"
Height="32"
Margin="0,8,0,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
CharacterSpacing="12"
FontSize="12"
Text="{x:Bind Title}"
Text="{x:Bind Title, Mode=OneWay}"
TextAlignment="Center"
TextTrimming="WordEllipsis"
TextWrapping="Wrap"
Visibility="{Binding ElementName=PageRoot, Path=DataContext.GridProperties.ShowTitle, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
</StackPanel>
Visibility="{x:Bind LayoutShowsTitle, Mode=OneWay}" />
</Grid>
</DataTemplate>
<DataTemplate x:Key="GalleryGridItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
@@ -193,11 +316,11 @@
Padding="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
AutomationProperties.Name="{x:Bind Title}"
AutomationProperties.Name="{x:Bind Title, Mode=OneWay}"
BorderThickness="0"
CornerRadius="4"
CornerRadius="{StaticResource GalleryGridViewItemRadius}"
Orientation="Vertical"
ToolTipService.ToolTip="{x:Bind Title}">
ToolTipService.ToolTip="{x:Bind Title, Mode=OneWay}">
<Grid
Width="160"
@@ -205,12 +328,8 @@
Margin="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
CornerRadius="4">
<Grid.RowDefinitions>
<RowDefinition />
</Grid.RowDefinitions>
CornerRadius="{StaticResource GalleryGridViewItemRadius}">
<Viewbox
Grid.Row="1"
HorizontalAlignment="Center"
Stretch="UniformToFill"
StretchDirection="Both">
@@ -222,35 +341,39 @@
</Viewbox>
</Grid>
<StackPanel Padding="4" Orientation="Vertical">
<StackPanel
Padding="4"
Orientation="Vertical"
Spacing="4"
Visibility="{x:Bind help:BindTransformers.VisibleWhenAny(ShowTitle, ShowSubtitle)}">
<TextBlock
x:Name="TitleTextBlock"
MaxWidth="152"
MaxHeight="40"
Margin="0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
CharacterSpacing="12"
FontSize="14"
Foreground="{ThemeResource TextFillColorPrimary}"
Text="{x:Bind Title}"
Text="{x:Bind Title, Mode=OneWay}"
TextAlignment="Center"
TextTrimming="WordEllipsis"
TextWrapping="NoWrap" />
TextWrapping="NoWrap"
Visibility="{x:Bind ShowTitle, Mode=OneWay}" />
<TextBlock
x:Name="SubTitleTextBlock"
MaxWidth="152"
MaxHeight="40"
Margin="0,4,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
CharacterSpacing="11"
FontSize="11"
Foreground="{ThemeResource TextFillColorTertiary}"
Text="{x:Bind Subtitle}"
Text="{x:Bind Subtitle, Mode=OneWay}"
TextAlignment="Center"
TextTrimming="WordEllipsis"
TextWrapping="NoWrap" />
TextWrapping="NoWrap"
Visibility="{x:Bind ShowSubtitle, Mode=OneWay}" />
</StackPanel>
</StackPanel>
</DataTemplate>
@@ -295,6 +418,7 @@
IsDoubleTapEnabled="True"
IsItemClickEnabled="True"
ItemClick="Items_ItemClick"
ItemContainerStyleSelector="{StaticResource GridItemContainerStyleSelector}"
ItemTemplateSelector="{StaticResource GridItemTemplateSelector}"
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
RightTapped="Items_RightTapped"
@@ -302,6 +426,7 @@
<GridView.ItemContainerTransitions>
<TransitionCollection />
</GridView.ItemContainerTransitions>
<GridView.ItemContainerStyle />
</GridView>
</controls:Case>
</controls:SwitchPresenter>

View File

@@ -15,4 +15,7 @@ internal static class BindTransformers
public static Visibility EmptyOrWhitespaceToCollapsed(string? input)
=> string.IsNullOrWhiteSpace(input) ? Visibility.Collapsed : Visibility.Visible;
public static Visibility VisibleWhenAny(bool value1, bool value2)
=> (value1 || value2) ? Visibility.Visible : Visibility.Collapsed;
}

View File

@@ -9,13 +9,6 @@ namespace SamplePagesExtension;
internal sealed partial class SampleGalleryListPage : ListPage
{
public SampleGalleryListPage()
{
Icon = new IconInfo("\uE7C5");
Name = "Sample Gallery List Page";
GridProperties = new GalleryGridLayout();
}
public override IListItem[] GetItems()
{
return [

View File

@@ -0,0 +1,59 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace SamplePagesExtension;
internal sealed partial class SampleGridsListPage : ListPage
{
private readonly IListItem[] _items =
[
new ListItem(new SampleGalleryListPage { GridProperties = new GalleryGridLayout { ShowTitle = true, ShowSubtitle = true } })
{
Title = "Gallery list page (title and subtitle)",
Subtitle = "A sample gallery list page with images",
Icon = IconHelpers.FromRelativePath("Assets/Images/Swirls.png"),
},
new ListItem(new SampleGalleryListPage { GridProperties = new GalleryGridLayout { ShowTitle = true, ShowSubtitle = false } })
{
Title = "Gallery list page (title, no subtitle)",
Subtitle = "A sample gallery list page with images",
Icon = IconHelpers.FromRelativePath("Assets/Images/Swirls.png"),
},
new ListItem(new SampleGalleryListPage { GridProperties = new GalleryGridLayout { ShowTitle = false, ShowSubtitle = false } })
{
Title = "Gallery list page (no title, no subtitle)",
Subtitle = "A sample gallery list page with images",
Icon = IconHelpers.FromRelativePath("Assets/Images/Swirls.png"),
},
new ListItem(new SampleGalleryListPage { GridProperties = new SmallGridLayout() })
{
Title = "Small grid list page",
Subtitle = "A sample grid list page with text items",
Icon = IconHelpers.FromRelativePath("Assets/Images/Win-Digital.png"),
},
new ListItem(new SampleGalleryListPage { GridProperties = new MediumGridLayout { ShowTitle = true } })
{
Title = "Medium grid (with title)",
Subtitle = "A sample grid list page with text items",
Icon = IconHelpers.FromRelativePath("Assets/Images/Win-Digital.png"),
},
new ListItem(new SampleGalleryListPage { GridProperties = new MediumGridLayout { ShowTitle = false } })
{
Title = "Medium grid (hidden title)",
Subtitle = "A sample grid list page with text items",
Icon = IconHelpers.FromRelativePath("Assets/Images/Win-Digital.png"),
}
];
public SampleGridsListPage()
{
Icon = new IconInfo("\uE7C5");
Name = "Grid and gallery lists";
}
public override IListItem[] GetItems() => _items;
}

View File

@@ -34,9 +34,9 @@ public partial class SamplesListPage : ListPage
Title = "Dynamic List Page Command",
Subtitle = "Changes the list of items in response to the typed query",
},
new ListItem(new SampleGalleryListPage())
new ListItem(new SampleGridsListPage())
{
Title = "Gallery List Page Command",
Title = "Grid views and galleries",
Subtitle = "Displays items as a gallery",
},
new ListItem(new OnLoadPage())