Browse Source

Add DrawerHeaderTemplate and DrawerFooterTemplate properties to DrawerPage (#21008)

* Add DrawerHeaderTemplate and DrawerFooterTemplate properties to DrawerPage

* Updated sample
pull/21025/head
Javier Suárez 6 months ago
committed by GitHub
parent
commit
cdf129941c
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 16
      samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml
  2. 125
      samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml.cs
  3. 32
      src/Avalonia.Controls/Page/DrawerPage.cs
  4. 2
      src/Avalonia.Themes.Fluent/Controls/DrawerPage.xaml
  5. 2
      src/Avalonia.Themes.Simple/Controls/DrawerPage.xaml
  6. 187
      tests/Avalonia.Controls.UnitTests/DrawerPageTests.cs

16
samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml

@ -72,6 +72,14 @@
<ComboBoxItem Content="Purple" />
</ComboBox>
<TextBlock Text="Header Template" FontSize="12" Opacity="0.7" />
<ComboBox x:Name="HeaderTemplateCombo" SelectedIndex="0"
SelectionChanged="OnHeaderTemplateChanged" HorizontalAlignment="Stretch">
<ComboBoxItem Content="None (direct control)" />
<ComboBoxItem Content="Banner" />
<ComboBoxItem Content="Avatar card" />
</ComboBox>
<TextBlock Text="Footer Background" FontSize="12" Opacity="0.7" />
<ComboBox x:Name="FooterBgCombo" SelectedIndex="0"
SelectionChanged="OnFooterBgChanged" HorizontalAlignment="Stretch">
@ -82,6 +90,14 @@
<ComboBoxItem Content="Maroon" />
</ComboBox>
<TextBlock Text="Footer Template" FontSize="12" Opacity="0.7" />
<ComboBox x:Name="FooterTemplateCombo" SelectedIndex="0"
SelectionChanged="OnFooterTemplateChanged" HorizontalAlignment="Stretch">
<ComboBoxItem Content="None (direct control)" />
<ComboBoxItem Content="Version badge" />
<ComboBoxItem Content="Icon + label" />
</ComboBox>
<TextBlock Text="Drawer Icon" FontSize="12" Opacity="0.7" />
<ComboBox x:Name="IconCombo" SelectedIndex="0"
SelectionChanged="OnIconChanged" HorizontalAlignment="Stretch">

125
samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml.cs

@ -1,8 +1,10 @@
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input.GestureRecognizers;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Media;
namespace ControlCatalog.Pages
@ -164,7 +166,7 @@ namespace ControlCatalog.Pages
if (!_isLoaded)
return;
if (ShowHeaderCheck.IsChecked == true)
DemoDrawer.DrawerHeader = DrawerHeaderBorder;
DemoDrawer.DrawerHeader = HeaderTemplateCombo.SelectedIndex == 0 ? DrawerHeaderBorder : (object)"My Application";
else
DemoDrawer.DrawerHeader = null;
}
@ -174,9 +176,128 @@ namespace ControlCatalog.Pages
if (!_isLoaded)
return;
if (ShowFooterCheck.IsChecked == true)
DemoDrawer.DrawerFooter = DrawerFooterBorder;
{
DemoDrawer.DrawerFooter = FooterTemplateCombo.SelectedIndex switch
{
1 => (object)"v12.0",
2 => (object)"Avalonia",
_ => DrawerFooterBorder
};
}
else
{
DemoDrawer.DrawerFooter = null;
}
}
private void OnHeaderTemplateChanged(object? sender, SelectionChangedEventArgs e)
{
if (!_isLoaded)
return;
switch (HeaderTemplateCombo.SelectedIndex)
{
case 1:
DemoDrawer.DrawerHeader = "My Application";
DemoDrawer.DrawerHeaderTemplate = new FuncDataTemplate<string>((data, _) =>
new Border
{
Padding = new Avalonia.Thickness(16),
Child = new StackPanel
{
Spacing = 2,
Children =
{
new TextBlock { Text = data, FontSize = 18, FontWeight = FontWeight.SemiBold, Foreground = Brushes.White },
new TextBlock { Text = "Navigation", FontSize = 12, Foreground = Brushes.White, Opacity = 0.7 }
}
}
});
break;
case 2:
DemoDrawer.DrawerHeader = "My Application";
DemoDrawer.DrawerHeaderTemplate = new FuncDataTemplate<string>((data, _) =>
{
var initial = data?.Length > 0 ? data[0].ToString().ToUpperInvariant() : "?";
var avatar = new Border
{
Width = 40,
Height = 40,
CornerRadius = new Avalonia.CornerRadius(20),
Background = new SolidColorBrush(Color.Parse("#1976D2")),
Child = new TextBlock
{
Text = initial,
FontSize = 18,
FontWeight = FontWeight.Bold,
Foreground = Brushes.White,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
}
};
var label = new TextBlock { Text = data, FontSize = 14, FontWeight = FontWeight.SemiBold, VerticalAlignment = VerticalAlignment.Center };
var row = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 10 };
row.Children.Add(avatar);
row.Children.Add(label);
return new Border { Padding = new Avalonia.Thickness(12), Child = row };
});
break;
default:
DemoDrawer.DrawerHeader = DrawerHeaderBorder;
DemoDrawer.DrawerHeaderTemplate = null;
break;
}
}
private void OnFooterTemplateChanged(object? sender, SelectionChangedEventArgs e)
{
if (!_isLoaded)
return;
switch (FooterTemplateCombo.SelectedIndex)
{
case 1:
DemoDrawer.DrawerFooter = "v12.0";
DemoDrawer.DrawerFooterTemplate = new FuncDataTemplate<string>((data, _) =>
new Border
{
Padding = new Avalonia.Thickness(12, 8),
Child = new Border
{
Padding = new Avalonia.Thickness(8, 4),
CornerRadius = new Avalonia.CornerRadius(4),
Background = new SolidColorBrush(Color.Parse("#1976D2")),
Child = new TextBlock { Text = data, FontSize = 11, Foreground = Brushes.White, FontWeight = FontWeight.SemiBold }
}
});
break;
case 2:
DemoDrawer.DrawerFooter = "Avalonia";
DemoDrawer.DrawerFooterTemplate = new FuncDataTemplate<string>((data, _) =>
{
var icon = new PathIcon
{
Width = 14,
Height = 14,
Data = Geometry.Parse("M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z"),
Opacity = 0.5
};
var label = new TextBlock { Text = data, FontSize = 12, Opacity = 0.6, VerticalAlignment = VerticalAlignment.Center };
var row = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 6 };
row.Children.Add(icon);
row.Children.Add(label);
return new Border { Padding = new Avalonia.Thickness(14, 10), Child = row };
});
break;
default:
DemoDrawer.DrawerFooter = DrawerFooterBorder;
DemoDrawer.DrawerFooterTemplate = null;
break;
}
}
private void OnMenuItemClick(object? sender, RoutedEventArgs e)

32
src/Avalonia.Controls/Page/DrawerPage.cs

@ -124,6 +124,18 @@ namespace Avalonia.Controls
public static readonly StyledProperty<object?> DrawerFooterProperty =
AvaloniaProperty.Register<DrawerPage, object?>(nameof(DrawerFooter));
/// <summary>
/// Defines the <see cref="DrawerHeaderTemplate"/> property.
/// </summary>
public static readonly StyledProperty<IDataTemplate?> DrawerHeaderTemplateProperty =
AvaloniaProperty.Register<DrawerPage, IDataTemplate?>(nameof(DrawerHeaderTemplate));
/// <summary>
/// Defines the <see cref="DrawerFooterTemplate"/> property.
/// </summary>
public static readonly StyledProperty<IDataTemplate?> DrawerFooterTemplateProperty =
AvaloniaProperty.Register<DrawerPage, IDataTemplate?>(nameof(DrawerFooterTemplate));
/// <summary>
/// Defines the <see cref="DrawerIcon"/> property.
/// </summary>
@ -403,6 +415,7 @@ namespace Avalonia.Controls
/// <summary>
/// Gets or sets the header content displayed at the top of the drawer pane.
/// </summary>
[DependsOn(nameof(DrawerHeaderTemplate))]
public object? DrawerHeader
{
get => GetValue(DrawerHeaderProperty);
@ -412,12 +425,31 @@ namespace Avalonia.Controls
/// <summary>
/// Gets or sets the footer content displayed at the bottom of the drawer pane.
/// </summary>
[DependsOn(nameof(DrawerFooterTemplate))]
public object? DrawerFooter
{
get => GetValue(DrawerFooterProperty);
set => SetValue(DrawerFooterProperty, value);
}
/// <summary>
/// Gets or sets the data template used to display <see cref="DrawerHeader"/>.
/// </summary>
public IDataTemplate? DrawerHeaderTemplate
{
get => GetValue(DrawerHeaderTemplateProperty);
set => SetValue(DrawerHeaderTemplateProperty, value);
}
/// <summary>
/// Gets or sets the data template used to display <see cref="DrawerFooter"/>.
/// </summary>
public IDataTemplate? DrawerFooterTemplate
{
get => GetValue(DrawerFooterTemplateProperty);
set => SetValue(DrawerFooterTemplateProperty, value);
}
/// <summary>
/// Gets or sets the icon displayed in the drawer toggle button.
/// </summary>

2
src/Avalonia.Themes.Fluent/Controls/DrawerPage.xaml

@ -57,11 +57,13 @@
<ContentPresenter Name="PART_DrawerHeader"
DockPanel.Dock="Top"
Content="{TemplateBinding DrawerHeader}"
ContentTemplate="{TemplateBinding DrawerHeaderTemplate}"
Background="{TemplateBinding DrawerHeaderBackground}"
IsVisible="{TemplateBinding DrawerHeader, Converter={x:Static ObjectConverters.IsNotNull}}" />
<ContentPresenter Name="PART_DrawerFooter"
DockPanel.Dock="Bottom"
Content="{TemplateBinding DrawerFooter}"
ContentTemplate="{TemplateBinding DrawerFooterTemplate}"
Background="{TemplateBinding DrawerFooterBackground}"
IsVisible="{TemplateBinding DrawerFooter, Converter={x:Static ObjectConverters.IsNotNull}}" />
<ContentPresenter Name="PART_DrawerPresenter"

2
src/Avalonia.Themes.Simple/Controls/DrawerPage.xaml

@ -56,11 +56,13 @@
<ContentPresenter Name="PART_DrawerHeader"
DockPanel.Dock="Top"
Content="{TemplateBinding DrawerHeader}"
ContentTemplate="{TemplateBinding DrawerHeaderTemplate}"
Background="{TemplateBinding DrawerHeaderBackground}"
IsVisible="{TemplateBinding DrawerHeader, Converter={x:Static ObjectConverters.IsNotNull}}" />
<ContentPresenter Name="PART_DrawerFooter"
DockPanel.Dock="Bottom"
Content="{TemplateBinding DrawerFooter}"
ContentTemplate="{TemplateBinding DrawerFooterTemplate}"
Background="{TemplateBinding DrawerFooterBackground}"
IsVisible="{TemplateBinding DrawerFooter, Converter={x:Static ObjectConverters.IsNotNull}}" />
<ContentPresenter Name="PART_DrawerPresenter"

187
tests/Avalonia.Controls.UnitTests/DrawerPageTests.cs

@ -8,6 +8,7 @@ using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Threading;
using Avalonia.UnitTests;
@ -1183,6 +1184,192 @@ public class DrawerPageTests
}
}
public class HeaderFooterTemplateTests : ScopedTestBase
{
// Wires PART_DrawerHeader and PART_DrawerFooter to the drawer's properties.
// Other parts omitted; OnApplyTemplate uses Find (nullable) so they are safe to skip.
private static IControlTemplate MinimalPaneTemplate() =>
new FuncControlTemplate<DrawerPage>((dp, scope) =>
{
var header = new ContentPresenter { Name = "PART_DrawerHeader" }.RegisterInNameScope(scope);
header.Bind(ContentPresenter.ContentProperty, dp.GetObservable(DrawerPage.DrawerHeaderProperty));
header.Bind(ContentPresenter.ContentTemplateProperty, dp.GetObservable(DrawerPage.DrawerHeaderTemplateProperty));
var footer = new ContentPresenter { Name = "PART_DrawerFooter" }.RegisterInNameScope(scope);
footer.Bind(ContentPresenter.ContentProperty, dp.GetObservable(DrawerPage.DrawerFooterProperty));
footer.Bind(ContentPresenter.ContentTemplateProperty, dp.GetObservable(DrawerPage.DrawerFooterTemplateProperty));
return new StackPanel { Children = { header, footer } };
});
private static (DrawerPage dp, ContentPresenter header, ContentPresenter footer, TestRoot root) Create(
object? drawerHeader = null,
IDataTemplate? headerTemplate = null,
object? drawerFooter = null,
IDataTemplate? footerTemplate = null)
{
var dp = new DrawerPage
{
Template = MinimalPaneTemplate(),
DrawerHeader = drawerHeader,
DrawerHeaderTemplate = headerTemplate,
DrawerFooter = drawerFooter,
DrawerFooterTemplate = footerTemplate,
};
var root = new TestRoot { Child = dp };
dp.ApplyTemplate();
var header = dp.GetVisualDescendants().OfType<ContentPresenter>().First(x => x.Name == "PART_DrawerHeader");
var footer = dp.GetVisualDescendants().OfType<ContentPresenter>().First(x => x.Name == "PART_DrawerFooter");
return (dp, header, footer, root);
}
[Fact]
public void DrawerHeaderTemplate_IsForwardedToContentPresenter()
{
var template = new FuncDataTemplate<string>((_, _) => new TextBlock());
var (_, header, _, _) = Create(drawerHeader: "App", headerTemplate: template);
Assert.Same(template, header.ContentTemplate);
}
[Fact]
public void DrawerFooterTemplate_IsForwardedToContentPresenter()
{
var template = new FuncDataTemplate<string>((_, _) => new TextBlock());
var (_, _, footer, _) = Create(drawerFooter: "v1.0", footerTemplate: template);
Assert.Same(template, footer.ContentTemplate);
}
[Fact]
public void DrawerHeaderTemplate_RendersControlProducedByFactory()
{
var (_, header, _, _) = Create(
drawerHeader: "App",
headerTemplate: new FuncDataTemplate<string>((_, _) => new Canvas()));
header.UpdateChild();
Assert.IsType<Canvas>(header.Child);
}
[Fact]
public void DrawerFooterTemplate_RendersControlProducedByFactory()
{
var (_, _, footer, _) = Create(
drawerFooter: "v1.0",
footerTemplate: new FuncDataTemplate<string>((_, _) => new Canvas()));
footer.UpdateChild();
Assert.IsType<Canvas>(footer.Child);
}
[Fact]
public void DrawerHeaderTemplate_ReceivesDrawerHeaderAsData()
{
object? receivedData = null;
var (_, header, _, _) = Create(
drawerHeader: "MyTitle",
headerTemplate: new FuncDataTemplate<string>((data, _) =>
{
receivedData = data;
return new TextBlock { Text = data };
}));
header.UpdateChild();
Assert.Equal("MyTitle", receivedData);
}
[Fact]
public void DrawerFooterTemplate_ReceivesDrawerFooterAsData()
{
object? receivedData = null;
var (_, _, footer, _) = Create(
drawerFooter: "v2.0",
footerTemplate: new FuncDataTemplate<string>((data, _) =>
{
receivedData = data;
return new TextBlock { Text = data };
}));
footer.UpdateChild();
Assert.Equal("v2.0", receivedData);
}
[Fact]
public void DrawerHeaderTemplate_SwapTemplate_UpdatesContentPresenter()
{
var second = new FuncDataTemplate<string>((_, _) => new Border());
var (dp, header, _, _) = Create(
drawerHeader: "App",
headerTemplate: new FuncDataTemplate<string>((_, _) => new Canvas()));
header.UpdateChild();
Assert.IsType<Canvas>(header.Child);
dp.DrawerHeaderTemplate = second;
header.UpdateChild();
Assert.IsType<Border>(header.Child);
}
[Fact]
public void DrawerFooterTemplate_SwapTemplate_UpdatesContentPresenter()
{
var second = new FuncDataTemplate<string>((_, _) => new Border());
var (dp, _, footer, _) = Create(
drawerFooter: "v1.0",
footerTemplate: new FuncDataTemplate<string>((_, _) => new Canvas()));
footer.UpdateChild();
Assert.IsType<Canvas>(footer.Child);
dp.DrawerFooterTemplate = second;
footer.UpdateChild();
Assert.IsType<Border>(footer.Child);
}
[Fact]
public void DrawerHeaderTemplate_ClearingTemplate_FallsBackToDirectContent()
{
var directControl = new TextBlock { Text = "Direct" };
var (dp, header, _, _) = Create(
drawerHeader: directControl,
headerTemplate: new FuncDataTemplate<object>((_, _) => new Canvas()));
header.UpdateChild();
Assert.IsType<Canvas>(header.Child);
dp.DrawerHeaderTemplate = null;
header.UpdateChild();
Assert.Same(directControl, header.Child);
}
[Fact]
public void DrawerFooterTemplate_ClearingTemplate_FallsBackToDirectContent()
{
var directControl = new TextBlock { Text = "Direct" };
var (dp, _, footer, _) = Create(
drawerFooter: directControl,
footerTemplate: new FuncDataTemplate<object>((_, _) => new Canvas()));
footer.UpdateChild();
Assert.IsType<Canvas>(footer.Child);
dp.DrawerFooterTemplate = null;
footer.UpdateChild();
Assert.Same(directControl, footer.Child);
}
}
public class SwipeGestureTests : ScopedTestBase
{
[Fact]

Loading…
Cancel
Save