Skip to content

Commit

Permalink
Add support for ShellContent.ContentTemplate property (#94)
Browse files Browse the repository at this point in the history
  • Loading branch information
Dreamescaper committed Jan 8, 2023
1 parent 637748e commit 78ec41e
Show file tree
Hide file tree
Showing 8 changed files with 243 additions and 22 deletions.
13 changes: 9 additions & 4 deletions samples/ControlGallery/AppShell.razor
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,15 @@
<FlexLayoutPage />

@*Shell*@
<ShellPropertiesPage @bind-ShellBackgroundColor="_backgroundColor"
@bind-ShellForegroundColor="_foregroundColor"
@bind-ShellUnselectedColor="_unselectedColor"
@bind-ShellTabBarTitleColor="_tabBarTitleColor" />
<ShellContent Title="Shell Properties">
<ContentTemplate>
<ShellPropertiesPage @bind-ShellBackgroundColor="_backgroundColor"
@bind-ShellForegroundColor="_foregroundColor"
@bind-ShellUnselectedColor="_unselectedColor"
@bind-ShellTabBarTitleColor="_tabBarTitleColor" />
</ContentTemplate>
</ShellContent>

<ShellItemsPage />

@*Navigation*@
Expand Down
20 changes: 12 additions & 8 deletions samples/ControlGallery/Views/Shell/ShellItemsPage.razor
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
<FlyoutItem Title="Shell Items">
<Tab Title="Add/Remove">
<ContentPage>
<StackLayout Spacing="12">
<Button OnClick="AddTab">Add Tab</Button>
<Button IsEnabled="(tabCount > 1)" OnClick="RemoveTab">Remove tab</Button>
<Button OnClick="AddMenuItem">Add MenuItem</Button>
<Button IsEnabled="(menuItemCount > 0)" OnClick="RemoveMenuItem">Remove MenuItem</Button>
</StackLayout>
</ContentPage>
<ShellContent>
<ContentTemplate>
<ContentPage Title="Shell Items">
<StackLayout Spacing="12">
<Button OnClick="AddTab">Add Tab</Button>
<Button IsEnabled="(tabCount > 1)" OnClick="RemoveTab">Remove tab</Button>
<Button OnClick="AddMenuItem">Add MenuItem</Button>
<Button IsEnabled="(menuItemCount > 0)" OnClick="RemoveMenuItem">Remove MenuItem</Button>
</StackLayout>
</ContentPage>
</ContentTemplate>
</ShellContent>
</Tab>

@for (int i = 0; i < tabCount; i++)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@

namespace BlazorBindings.Maui.Elements.DataTemplates
{
#pragma warning disable CA1812 // Avoid uninstantiated internal classes. Class is used as generic parameter.
/// <summary>
/// This ControlTemplate implementation wraps the content in an additional View, therefore it is not suitable in cases when non-View content
/// is expected from template.
/// </summary>
internal class ControlTemplateItemsComponent<T> : NativeControlComponentBase, IMauiContainerElementHandler, INonChildContainerElement
where T : MC.BindableObject
#pragma warning restore CA1812 // Avoid uninstantiated internal classes
{
protected override RenderFragment GetChildContent()
{
Expand Down Expand Up @@ -66,9 +68,7 @@ void INonPhysicalChild.SetParent(object parentElement)
}

void INonPhysicalChild.RemoveFromParent(object parentElement) { }

void IElementHandler.ApplyAttribute(ulong attributeEventHandlerId, string attributeName, object attributeValue, string attributeEventUpdatesAttributeName) { }

void IMauiContainerElementHandler.AddChild(MC.BindableObject child, int physicalSiblingIndex) { }
void IMauiContainerElementHandler.RemoveChild(MC.BindableObject child) { }
int IMauiContainerElementHandler.GetChildIndex(MC.BindableObject child) => _itemRoots.IndexOf((MC.VerticalStackLayout)child);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

using BlazorBindings.Core;
using BlazorBindings.Maui.Elements.Internal;
using Microsoft.AspNetCore.Components;
using System;
using System.Linq;
using MC = Microsoft.Maui.Controls;

namespace BlazorBindings.Maui.Elements.DataTemplates
{
/// <summary>
/// Unlike <see cref="ControlTemplateItemsComponent{T}"/>, this DataTemplate component does not use a wrapping element.
/// This makes it possible to use when returning a View from template is not an option.
/// However, it requires a DataTemplate to render synchronously, which does not always work with Blazor.
/// </summary>
internal class SyncControlTemplateItemsComponent<T> : NativeControlComponentBase, IMauiContainerElementHandler, INonChildContainerElement
where T : MC.BindableObject
{
protected override RenderFragment GetChildContent()
{
return builder =>
{
for (int i = 0; i < _count; i++)
{
builder.OpenComponent<RootContainerComponent>(1);
builder.AddAttribute(2, nameof(RootContainerComponent.ChildContent), Template);
builder.AddComponentReferenceCapture(3, c => _lastRootContainer = (RootContainerComponent)c);
builder.CloseComponent();
}
};
}

[Parameter] public Action<T, MC.ControlTemplate> SetControlTemplateAction { get; set; }
[Parameter] public Action<T, MC.DataTemplate> SetDataTemplateAction { get; set; }
[Parameter] public RenderFragment Template { get; set; }

private RootContainerComponent _lastRootContainer;
private int _count;

private Microsoft.Maui.IView AddTemplateRoot()
{
_count++;
StateHasChanged();

var rootElement = _lastRootContainer?.Elements?.FirstOrDefault()
?? throw new InvalidOperationException("Template root control is supposed to be rendered at this point.");
_lastRootContainer = null;

return (Microsoft.Maui.IView)rootElement;
}

void INonPhysicalChild.SetParent(object parentElement)
{
var parent = (T)parentElement;

if (SetControlTemplateAction != null)
{
var controlTemplate = new MC.ControlTemplate(AddTemplateRoot);
SetControlTemplateAction(parent, controlTemplate);
}

if (SetDataTemplateAction != null)
{
var dataTemplate = new MC.DataTemplate(AddTemplateRoot);
SetDataTemplateAction(parent, dataTemplate);
}
}

void INonPhysicalChild.RemoveFromParent(object parentElement) { }

void IElementHandler.ApplyAttribute(ulong attributeEventHandlerId, string attributeName, object attributeValue, string attributeEventUpdatesAttributeName) { }

void IMauiContainerElementHandler.AddChild(MC.BindableObject child, int physicalSiblingIndex) { }
void IMauiContainerElementHandler.RemoveChild(MC.BindableObject child) { }
int IMauiContainerElementHandler.GetChildIndex(MC.BindableObject child) => -1;
MC.BindableObject IMauiElementHandler.ElementControl => null;
object IElementHandler.TargetElement => null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

using BlazorBindings.Core;
using Microsoft.AspNetCore.Components;
using System;
using System.Collections.Generic;
using MC = Microsoft.Maui.Controls;

namespace BlazorBindings.Maui.Elements.Internal
{
internal class RootContainerComponent : NativeControlComponentBase, IMauiContainerElementHandler, INonChildContainerElement
{
[Parameter] public RenderFragment ChildContent { get; set; }
protected override RenderFragment GetChildContent() => ChildContent;

public List<MC.BindableObject> Elements { get; } = new List<MC.BindableObject>();

void IMauiContainerElementHandler.AddChild(MC.BindableObject child, int physicalSiblingIndex)
{
var index = Math.Min(physicalSiblingIndex, Elements.Count);
Elements.Insert(index, child);
}

void IMauiContainerElementHandler.RemoveChild(MC.BindableObject child)
{
Elements.Remove(child);
}

int IMauiContainerElementHandler.GetChildIndex(MC.BindableObject child)
{
return Elements.IndexOf(child);
}

// Because this is a 'fake' container element, all matters related to physical trees
// should be no-ops.
MC.BindableObject IMauiElementHandler.ElementControl => null;
object IElementHandler.TargetElement => null;
void IElementHandler.ApplyAttribute(ulong attributeEventHandlerId, string attributeName, object attributeValue, string attributeEventUpdatesAttributeName) { }
void INonPhysicalChild.SetParent(object parentElement) { }
void INonPhysicalChild.RemoveFromParent(object parentElement) { }
}
}
20 changes: 18 additions & 2 deletions src/BlazorBindings.Maui/Elements/ShellContent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@
// Licensed under the MIT license.

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using MC = Microsoft.Maui.Controls;

namespace BlazorBindings.Maui.Elements
{
public partial class ShellContent : BaseShellItem, IMauiContainerElementHandler
{
#pragma warning disable CA1721 // Property names should not match get methods
/// <summary>
/// Gets or sets a data template to create when ShellContent becomes active.
/// </summary>
[Parameter] public RenderFragment ContentTemplate { get; set; }

[Parameter] public RenderFragment ChildContent { get; set; }
#pragma warning restore CA1721 // Property names should not match get methods

protected override RenderFragment GetChildContent() => ChildContent;

Expand All @@ -21,6 +25,11 @@ protected override bool HandleAdditionalParameter(string name, object value)
ChildContent = (RenderFragment)value;
return true;
}
if (name == nameof(ContentTemplate))
{
ContentTemplate = (RenderFragment)value;
return true;
}
else
{
return base.HandleAdditionalParameter(name, value);
Expand All @@ -44,5 +53,12 @@ void IMauiContainerElementHandler.RemoveChild(MC.BindableObject child)
NativeControl.Content = null;
}
}

protected override void RenderAdditionalPartialElementContent(RenderTreeBuilder builder, ref int sequence)
{
base.RenderAdditionalPartialElementContent(builder, ref sequence);

RenderTreeBuilderHelper.AddSyncDataTemplateProperty<MC.ShellContent>(builder, sequence++, ContentTemplate, (x, template) => x.ContentTemplate = template);
}
}
}
19 changes: 17 additions & 2 deletions src/BlazorBindings.Maui/RenderTreeBuilderHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,24 @@ public static void AddControlTemplateProperty<T>(
}
}

private static string GetElementName(Type containingType, string propertyName)
internal static void AddSyncDataTemplateProperty<T>(
RenderTreeBuilder builder,
int sequence,
RenderFragment template,
Action<T, MC.DataTemplate> setDataTemplateAction)
where T : MC.BindableObject
{
return $"p-{containingType.FullName}.{propertyName}";
if (template != null)
{
builder.OpenRegion(sequence);

builder.OpenComponent<SyncControlTemplateItemsComponent<T>>(0);
builder.AddAttribute(1, nameof(SyncControlTemplateItemsComponent<T>.SetDataTemplateAction), setDataTemplateAction);
builder.AddAttribute(2, nameof(SyncControlTemplateItemsComponent<T>.Template), template);
builder.CloseComponent();

builder.CloseRegion();
}
}
}
}
60 changes: 58 additions & 2 deletions src/BlazorBindings.UnitTests/Elements/ShellTests.razor
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
@inherits ElementTestBase
@using BlazorBindings.UnitTests.Components;

@inherits ElementTestBase
@code {

[Test]
Expand All @@ -15,7 +17,8 @@
TabBarForegroundColor="Colors.IndianRed"
TabBarTitleColor="Colors.NavajoWhite"
TabBarUnselectedColor="Colors.Khaki">
</Shell>);
</Shell>
);

Assert.That(MC.Shell.GetBackgroundColor(shell), Is.EqualTo(Colors.AliceBlue));
Assert.That(MC.Shell.GetDisabledColor(shell), Is.EqualTo(Colors.Beige));
Expand All @@ -29,4 +32,57 @@
Assert.That(MC.Shell.GetTabBarTitleColor(shell), Is.EqualTo(Colors.NavajoWhite));
Assert.That(MC.Shell.GetTabBarUnselectedColor(shell), Is.EqualTo(Colors.Khaki));
}

[Test]
public async Task AddPageDirectlyToShell()
{
var shell = await Render<MC.Shell>(
@<Shell>
<PageContent />
</Shell>
);

var shellContent = shell.CurrentItem.Items[0].Items[0];
var page = ((MC.IShellContentController)shellContent).GetOrCreateContent();

PageContent.ValidateContent(page);
}

[Test]
public async Task AddShellContent()
{
var shell = await Render<MC.Shell>(
@<Shell>
<ShellContent Title="Some Page">
<PageContent />
</ShellContent>
</Shell>
);

var shellContent = shell.CurrentItem.Items[0].Items[0];
var page = ((MC.IShellContentController)shellContent).GetOrCreateContent();

Assert.That(shellContent.Title, Is.EqualTo("Some Page"));
PageContent.ValidateContent(page);
}

[Test]
public async Task AddShellContentWithTemplate()
{
var shell = await Render<MC.Shell>(
@<Shell>
<ShellContent Title="Some Page">
<ContentTemplate>
<PageContent />
</ContentTemplate>
</ShellContent>
</Shell>
);

var shellContent = shell.CurrentItem.Items[0].Items[0];
var page = ((MC.IShellContentController)shellContent).GetOrCreateContent();

Assert.That(shellContent.Title, Is.EqualTo("Some Page"));
PageContent.ValidateContent(page);
}
}

0 comments on commit 78ec41e

Please sign in to comment.