diff --git a/.ai-team/agents/cyclops/history.md b/.ai-team/agents/cyclops/history.md index 7363ca5d..384e6524 100644 --- a/.ai-team/agents/cyclops/history.md +++ b/.ai-team/agents/cyclops/history.md @@ -16,6 +16,12 @@ - **Pre-existing test infrastructure issue:** The test project on `dev` has a broken `AddXUnit` reference in `BlazorWebFormsTestContext.cs` — this is not caused by component changes. - **FileUpload must use InputFile internally:** Raw `` with `@onchange` receives `ChangeEventArgs` (no file data). Must use Blazor's `InputFile` component which provides `InputFileChangeEventArgs` with `IBrowserFile` objects. The `@using Microsoft.AspNetCore.Components.Forms` directive is needed in the `.razor` file since `_Imports.razor` only imports `Microsoft.AspNetCore.Components.Web`. - **Path security in file save operations:** `Path.Combine` silently drops earlier arguments if a later argument is rooted (e.g., `Path.Combine("uploads", "/etc/passwd")` returns `/etc/passwd`). Always use `Path.GetFileName()` to sanitize filenames and validate resolved paths with `Path.GetFullPath()` + `StartsWith()` check. +- **PageService event handler catch pattern:** In `Page.razor.cs`, async event handlers that call `InvokeAsync(StateHasChanged)` should catch `ObjectDisposedException` (not generic `Exception`) — the component may be disposed during navigation while an event is still in flight. This is the standard Blazor pattern for disposed-component safety. +- **Test dead code:** Code scanning flags unused variable assignments in test files. Use `_ = expr` discard for side-effect-only calls, and remove `var` assignments where the result is never asserted. +- **ImageMap base class fix:** ImageMap inherits `BaseStyledComponent` (not `BaseWebFormsComponent`), matching the Web Forms `ImageMap → Image → WebControl` hierarchy. This gives it CssClass, Style, Font, BackColor, etc. The `@inherits` directive in `.razor` must match the code-behind. +- **Instance-based IDs for generated HTML IDs:** Never use `static` counters for internal element IDs (like map names) — they leak across test runs and create non-deterministic output. Use `Guid.NewGuid()` as a field initializer instead. +- **ImageAlign rendering:** `.ToString().ToLower()` on `ImageAlign` enum values produces the correct Web Forms output (`absbottom`, `absmiddle`, `texttop`). No custom mapping needed. +- **Enabled propagation pattern:** When `Enabled=false` on a styled component, interactive child elements (like `` in ImageMap) should render as inactive (nohref, no onclick). Check `Enabled` from `BaseWebFormsComponent` — it defaults to `true`. 📌 Team update (2026-02-10): FileUpload needs InputFile integration — @onchange won't populate file data. Ship-blocking bug. — decided by Forge 📌 Team update (2026-02-10): ImageMap base class must be BaseStyledComponent, not BaseWebFormsComponent — decided by Forge diff --git a/README.md b/README.md index 5173e197..f66eb4a9 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ There are a handful of features that augment the ASP.NET developmen - [DataBinder](docs/UtilityFeatures/Databinder.md) - [ID Rendering](docs/UtilityFeatures/IDRendering.md) - Render HTML IDs for JavaScript integration - [JavaScript Setup](docs/UtilityFeatures/JavaScriptSetup.md) - Options for auto-loading required JavaScript + - [PageService](docs/UtilityFeatures/PageService.md) - Programmatic page title setting (Page.Title equivalent) - [ViewState](docs/UtilityFeatures/ViewState.md) ### Custom Control Migration Support diff --git a/docs/UtilityFeatures/PageService.md b/docs/UtilityFeatures/PageService.md new file mode 100644 index 00000000..0701f3b1 --- /dev/null +++ b/docs/UtilityFeatures/PageService.md @@ -0,0 +1,305 @@ +# PageService + +## Background + +In ASP.NET Web Forms, the `Page` object provided a central place for page-level properties and functionality. Key properties included `Page.Title`, `Page.MetaDescription`, and `Page.MetaKeywords`, which allowed developers to programmatically set page metadata that appears in the browser and search engine results. + +```csharp +// Web Forms code-behind +protected void Page_Load(object sender, EventArgs e) +{ + Page.Title = "My Dynamic Page Title"; + Page.MetaDescription = "Description for search engines"; + Page.MetaKeywords = "keyword1, keyword2, keyword3"; +} + +protected void UpdateButton_Click(object sender, EventArgs e) +{ + Page.Title = "Title Updated - " + DateTime.Now.ToString(); + Page.MetaDescription = GetDescriptionFromDatabase(); +} +``` + +This pattern was essential for: +- Setting page titles dynamically based on data or user actions +- Implementing SEO-friendly titles and descriptions for content pages +- Providing context-specific metadata in master page scenarios +- Improving search engine visibility and social media sharing + +## Web Forms Usage + +In Web Forms, the `Page` object was automatically available in all code-behind files: + +```aspx +<%@ Page Language="C#" Title="Static Title" + MetaDescription="Page description" + MetaKeywords="keyword1, keyword2" %> +``` + +```csharp +// Code-behind (.aspx.cs) +public partial class MyPage : System.Web.UI.Page +{ + protected void Page_Load(object sender, EventArgs e) + { + // Dynamically set page metadata + Page.Title = GetTitleFromDatabase(); + Page.MetaDescription = GetDescriptionFromDatabase(); + Page.MetaKeywords = GetKeywordsFromDatabase(); + } + + private string GetTitleFromDatabase() + { + // Fetch from database + return "Dynamic Title from DB"; + } + + private string GetDescriptionFromDatabase() + { + return "This is a dynamic description for SEO"; + } + + private string GetKeywordsFromDatabase() + { + return "blazor, webforms, migration, seo"; + } +} +``` + +These properties would automatically update the HTML `` and `<meta>` elements in the rendered page. + +## Blazor Implementation + +BlazorWebFormsComponents provides `IPageService` and `PageService` to replicate this functionality in Blazor. The service is registered as a scoped service (one instance per request/render cycle) and can be injected into any component or page. + +### Key Components + +1. **IPageService Interface** - Defines the contract for page-level services +2. **PageService Class** - Default implementation providing `Title`, `MetaDescription`, and `MetaKeywords` properties +3. **Page Component** - Blazor component that renders the dynamic `<PageTitle>` and `<meta>` tags + +### Registration + +The service is automatically registered when you call `AddBlazorWebFormsComponents()`: + +```csharp +// Program.cs +builder.Services.AddBlazorWebFormsComponents(); +``` + +This registers `IPageService` as a scoped service that can be injected into components. + +### Usage in Blazor + +**Step 1: Add the Page component to your page or layout** + +```razor +@inject IPageService Page + +<BlazorWebFormsComponents.Page /> @* This renders the dynamic <PageTitle> and meta tags *@ +``` + +**Step 2: Set page properties programmatically** + +```razor +@code { + protected override void OnInitialized() + { + Page.Title = "My Dynamic Page Title"; + Page.MetaDescription = "Description for search engines"; + Page.MetaKeywords = "blazor, webforms, migration"; + } + + private void UpdateMetadata() + { + Page.Title = "Updated Title - " + DateTime.Now.ToString(); + Page.MetaDescription = "Updated description based on user action"; + } +} +``` + +### Complete Example + +```razor +@page "/MyPage" +@inject IPageService Page + +<BlazorWebFormsComponents.Page /> + +<h1>My Page</h1> + +<div> + <label>New Title:</label> + <input @bind="newTitle" /> + <label>New Description:</label> + <textarea @bind="newDescription"></textarea> + <button @onclick="UpdatePageMetadata">Update Metadata</button> +</div> + +@code { + private string newTitle = ""; + private string newDescription = ""; + + protected override void OnInitialized() + { + Page.Title = "My Page - BlazorWebFormsComponents"; + Page.MetaDescription = "A sample page demonstrating PageService"; + Page.MetaKeywords = "blazor, sample, demo"; + } + + private void UpdatePageMetadata() + { + if (!string.IsNullOrWhiteSpace(newTitle)) + { + Page.Title = newTitle; + Page.MetaDescription = newDescription; + } + } +} +``` + +## Migration Path + +### Before (Web Forms) + +```aspx +<%@ Page Language="C#" MasterPageFile="~/Site.Master" + MetaDescription="Customer details page" + MetaKeywords="customer, details, crm" %> + +<script runat="server"> + protected void Page_Load(object sender, EventArgs e) + { + var customerName = GetCustomerName(); + Page.Title = "Customer Details - " + customerName; + Page.MetaDescription = $"View details for {customerName}"; + } +</script> +``` + +### After (Blazor) + +```razor +@page "/customer/{id:int}" +@inject IPageService Page + +<BlazorWebFormsComponents.Page /> + +<h1>Customer Details</h1> + +@code { + [Parameter] + public int Id { get; set; } + + protected override async Task OnInitializedAsync() + { + var customerName = await GetCustomerName(Id); + Page.Title = $"Customer Details - {customerName}"; + Page.MetaDescription = $"View detailed information for {customerName}"; + Page.MetaKeywords = "customer, details, crm"; + } +} +``` + +## Features + +### Title Property + +- **Get/Set**: Read and write the page title dynamically +- **Event-Driven**: `TitleChanged` event fires when title is updated +- **Reactive**: The `Page` component automatically updates the browser title when the property changes + +### MetaDescription Property + +- **Get/Set**: Read and write the meta description dynamically +- **SEO-Friendly**: Appears in search engine results (recommended 150-160 characters) +- **Event-Driven**: `MetaDescriptionChanged` event fires when description is updated +- **Reactive**: The `Page` component automatically updates the meta tag when the property changes + +### MetaKeywords Property + +- **Get/Set**: Read and write the meta keywords dynamically +- **SEO Support**: Helps categorize page content for search engines +- **Event-Driven**: `MetaKeywordsChanged` event fires when keywords are updated +- **Reactive**: The `Page` component automatically updates the meta tag when the property changes + +### Future Extensibility + +The `IPageService` interface can be extended in future versions to support additional `Page` object features: + +- Open Graph meta tags for social media +- Page-level client script registration +- Page-level CSS registration +- Custom meta tags + +## Key Differences from Web Forms + +| Web Forms | Blazor with PageService | Notes | +|-----------|------------------------|-------| +| `Page.Title` property | `Page.Title` property | Same API! Just inject `IPageService` as `Page` | +| `Page.MetaDescription` property | `Page.MetaDescription` property | Available in Web Forms .NET 4.0+ | +| `Page.MetaKeywords` property | `Page.MetaKeywords` property | Available in Web Forms .NET 4.0+ | +| Available automatically | Must inject `IPageService` as `Page` | Standard Blazor DI pattern | +| Synchronous | Synchronous | No change needed | +| Scoped to request | Scoped to render cycle | Similar lifecycle | + +## Moving On + +While `PageService` provides familiar Web Forms compatibility, consider these Blazor-native approaches: + +### For Static Metadata + +Use the built-in components directly: + +```razor +@page "/about" + +<PageTitle>About Us - My Company</PageTitle> +<HeadContent> + <meta name="description" content="Learn about our company" /> + <meta name="keywords" content="about, company, team" /> +</HeadContent> + +<h1>About Us</h1> +``` + +### For Dynamic Metadata + +The `PageService` approach is appropriate when: +- Metadata depends on data loaded asynchronously +- Metadata changes based on user actions +- Metadata is set in response to events +- You want Web Forms-style programmatic control + +For simpler scenarios, you can use built-in components with bound variables: + +```razor +<PageTitle>@currentTitle</PageTitle> +<HeadContent> + <meta name="description" content="@currentDescription" /> +</HeadContent> + +@code { + private string currentTitle = "Default Title"; + private string currentDescription = "Default description"; + + private void UpdateTitle(string newTitle) + { + currentTitle = newTitle; + } +} +``` + +## Best Practices + +1. **Set Title Early**: Set the title in `OnInitializedAsync` or `OnParametersSet` to ensure it's available before first render +2. **SEO Considerations**: Provide meaningful, descriptive titles for better search engine optimization +3. **User Context**: Include relevant context in the title (e.g., customer name, product name) +4. **Length**: Keep titles under 60 characters for optimal display in browser tabs and search results +5. **Consistent Pattern**: Use a consistent title format across your application (e.g., "Page Name - Site Name") + +## See Also + +- [Live Sample](https://blazorwebformscomponents.azurewebsites.net/UtilityFeatures/PageService) +- [Microsoft Docs: Page.Title Property](https://docs.microsoft.com/en-us/dotnet/api/system.web.ui.page.title?view=netframework-4.8) +- [Blazor PageTitle Component](https://docs.microsoft.com/en-us/aspnet/core/blazor/fundamentals/routing#page-title) diff --git a/mkdocs.yml b/mkdocs.yml index bbb1d7b8..baa8e5d3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -113,6 +113,7 @@ nav: - Databinder: UtilityFeatures/Databinder.md - ID Rendering: UtilityFeatures/IDRendering.md - JavaScript Setup: UtilityFeatures/JavaScriptSetup.md + - PageService: UtilityFeatures/PageService.md - ViewState: UtilityFeatures/ViewState.md - Migration: - Getting started: Migration/readme.md diff --git a/samples/AfterBlazorServerSide/Components/Layout/NavMenu.razor b/samples/AfterBlazorServerSide/Components/Layout/NavMenu.razor index 27a594ad..df1ee667 100644 --- a/samples/AfterBlazorServerSide/Components/Layout/NavMenu.razor +++ b/samples/AfterBlazorServerSide/Components/Layout/NavMenu.razor @@ -129,6 +129,9 @@ </TreeNode> + <TreeNode Text="Utility Features"> + <TreeNode Text="PageService" NavigateUrl="/UtilityFeatures/PageService"></TreeNode> + </TreeNode> <TreeNode Text="Migration Guides"> <TreeNode Text="MasterPages" NavigateUrl="/control-samples/masterpage"></TreeNode> </TreeNode> diff --git a/samples/AfterBlazorServerSide/Components/Pages/UtilityFeatures/PageService/Index.razor b/samples/AfterBlazorServerSide/Components/Pages/UtilityFeatures/PageService/Index.razor new file mode 100644 index 00000000..3b7bd710 --- /dev/null +++ b/samples/AfterBlazorServerSide/Components/Pages/UtilityFeatures/PageService/Index.razor @@ -0,0 +1,163 @@ +@page "/UtilityFeatures/PageService" +@inject IPageService Page + +<BlazorWebFormsComponents.Page /> + +<h2>PageService - Web Forms Page Object Compatibility</h2> + +<p>The <code>PageService</code> provides page-level services that mimic ASP.NET Web Forms <code>Page</code> object functionality, enabling programmatic access to page properties like <code>Title</code>, <code>MetaDescription</code>, and <code>MetaKeywords</code>.</p> + +<div class="demo-section"> + <h3>Current Page Properties</h3> + <p><strong>Title:</strong> @Page.Title</p> + <p><strong>Meta Description:</strong> @(string.IsNullOrEmpty(Page.MetaDescription) ? "(not set)" : Page.MetaDescription)</p> + <p><strong>Meta Keywords:</strong> @(string.IsNullOrEmpty(Page.MetaKeywords) ? "(not set)" : Page.MetaKeywords)</p> + + <div class="mb-3"> + <label class="form-label">Set New Page Title:</label> + <input type="text" class="form-control" style="max-width: 400px;" @bind="NewTitle" placeholder="Enter page title..." /> + </div> + + <div class="mb-3"> + <label class="form-label">Set Meta Description:</label> + <textarea class="form-control" style="max-width: 400px;" rows="2" @bind="NewMetaDescription" placeholder="Enter meta description..."></textarea> + </div> + + <div class="mb-3"> + <label class="form-label">Set Meta Keywords:</label> + <input type="text" class="form-control" style="max-width: 400px;" @bind="NewMetaKeywords" placeholder="Enter keywords (comma-separated)..." /> + </div> + + <Button Text="Update Page Properties" OnClick="UpdatePageProperties" CssClass="btn btn-primary" /> + + @if (!string.IsNullOrEmpty(Message)) + { + <div class="alert alert-success mt-3">@Message</div> + } +</div> + +<hr /> + +<h3>Web Forms Comparison</h3> + +<div class="row"> + <div class="col-md-6"> + <h4>ASP.NET Web Forms</h4> + <pre><code class="language-csharp">// In code-behind (.aspx.cs) +protected void Page_Load(object sender, EventArgs e) +{ + Page.Title = "My Dynamic Title"; + Page.MetaDescription = "Page description for SEO"; + Page.MetaKeywords = "blazor, webforms, migration"; +} + +protected void UpdateButton_Click(object sender, EventArgs e) +{ + Page.Title = txtNewTitle.Text; + Page.MetaDescription = txtDescription.Text; + Page.MetaKeywords = txtKeywords.Text; +}</code></pre> + </div> + <div class="col-md-6"> + <h4>Blazor with PageService</h4> + <pre><code class="language-razor">@@page "/MyPage" +@@inject IPageService Page + +<BlazorWebFormsComponents.Page /> + +@@code { + protected override void OnInitialized() + { + Page.Title = "My Dynamic Title"; + Page.MetaDescription = "Page description for SEO"; + Page.MetaKeywords = "blazor, webforms, migration"; + } + + private void UpdatePageProperties() + { + Page.Title = NewTitle; + Page.MetaDescription = NewMetaDescription; + Page.MetaKeywords = NewMetaKeywords; + } +}</code></pre> + </div> +</div> + +<hr /> + +<h3>Key Features</h3> +<ul> + <li><strong>Programmatic Title Setting:</strong> Set page title dynamically via code, just like <code>Page.Title</code> in Web Forms</li> + <li><strong>Meta Tags Support:</strong> Set meta description and keywords for SEO, equivalent to <code>Page.MetaDescription</code> and <code>Page.MetaKeywords</code></li> + <li><strong>Dependency Injection:</strong> Available through DI as <code>IPageService</code></li> + <li><strong>Event-Based Updates:</strong> The <code>Page</code> component automatically updates page metadata when properties change</li> + <li><strong>Scoped Service:</strong> One instance per request/render cycle, just like Web Forms <code>Page</code> object</li> +</ul> + +<h3>Usage Instructions</h3> + +<h4>1. Register the Service (Already done in this sample)</h4> +<pre><code class="language-csharp">// In Program.cs +builder.Services.AddBlazorWebFormsComponents();</code></pre> + +<h4>2. Add the Page Component to Your Layout or Page</h4> +<pre><code class="language-razor">@@inject IPageService Page + +<BlazorWebFormsComponents.Page /> @* This renders the dynamic PageTitle and meta tags *@</code></pre> + +<h4>3. Set Page Properties Programmatically</h4> +<pre><code class="language-csharp">@@code { + protected override void OnInitialized() + { + Page.Title = "My Page Title"; + Page.MetaDescription = "Description for search engines"; + Page.MetaKeywords = "keyword1, keyword2, keyword3"; + } + + private void SomeEventHandler() + { + Page.Title = "Updated Title"; + Page.MetaDescription = "Updated description"; + } +}</code></pre> + +<h3>SEO Benefits</h3> +<p>The <code>MetaDescription</code> and <code>MetaKeywords</code> properties help improve your site's search engine optimization:</p> +<ul> + <li><strong>Meta Description:</strong> Appears in search results and social media previews (recommended 150-160 characters)</li> + <li><strong>Meta Keywords:</strong> Helps categorize page content (use relevant, comma-separated keywords)</li> + <li><strong>Dynamic Updates:</strong> Set metadata based on page content, user context, or database values</li> +</ul> + +@code { + private string NewTitle = ""; + private string NewMetaDescription = ""; + private string NewMetaKeywords = ""; + private string Message = ""; + + protected override void OnInitialized() + { + // Set initial page properties + Page.Title = "PageService Sample - BlazorWebFormsComponents"; + Page.MetaDescription = "Demonstrates the PageService utility for setting page title and meta tags programmatically in Blazor, similar to ASP.NET Web Forms Page object."; + Page.MetaKeywords = "blazor, webforms, page service, meta tags, seo, migration"; + } + + private void UpdatePageProperties() + { + if (!string.IsNullOrWhiteSpace(NewTitle)) + { + Page.Title = NewTitle; + Page.MetaDescription = NewMetaDescription; + Page.MetaKeywords = NewMetaKeywords; + Message = $"Page properties updated!"; + NewTitle = ""; + NewMetaDescription = ""; + NewMetaKeywords = ""; + } + else + { + Message = "Please enter at least a title"; + } + } +} diff --git a/src/BlazorWebFormsComponents.Test/PageService/BasicTests.razor b/src/BlazorWebFormsComponents.Test/PageService/BasicTests.razor new file mode 100644 index 00000000..33d6c001 --- /dev/null +++ b/src/BlazorWebFormsComponents.Test/PageService/BasicTests.razor @@ -0,0 +1,74 @@ +@code { + + [Fact] + public void PageService_DefaultTitle_IsEmpty() + { + // Arrange + var pageService = new PageService(); + + // Assert + pageService.Title.ShouldBe(string.Empty); + } + + [Fact] + public void PageService_SetTitle_UpdatesValue() + { + // Arrange + var pageService = new PageService(); + + // Act + pageService.Title = "Test Page Title"; + + // Assert + pageService.Title.ShouldBe("Test Page Title"); + } + + [Fact] + public void PageService_SetTitle_RaisesTitleChangedEvent() + { + // Arrange + var pageService = new PageService(); + string? capturedTitle = null; + pageService.TitleChanged += (sender, title) => capturedTitle = title; + + // Act + pageService.Title = "New Title"; + + // Assert + capturedTitle.ShouldBe("New Title"); + } + + [Fact] + public void PageService_SetSameTitle_DoesNotRaiseEvent() + { + // Arrange + var pageService = new PageService(); + pageService.Title = "Initial Title"; + int eventCount = 0; + pageService.TitleChanged += (sender, title) => eventCount++; + + // Act + pageService.Title = "Initial Title"; + + // Assert + eventCount.ShouldBe(0); + } + + [Fact] + public void PageService_SetDifferentTitle_RaisesEventAgain() + { + // Arrange + var pageService = new PageService(); + pageService.Title = "First Title"; + int eventCount = 0; + pageService.TitleChanged += (sender, title) => eventCount++; + + // Act + pageService.Title = "Second Title"; + pageService.Title = "Third Title"; + + // Assert + eventCount.ShouldBe(2); + } + +} diff --git a/src/BlazorWebFormsComponents.Test/PageService/ComponentTests.razor b/src/BlazorWebFormsComponents.Test/PageService/ComponentTests.razor new file mode 100644 index 00000000..78da03d1 --- /dev/null +++ b/src/BlazorWebFormsComponents.Test/PageService/ComponentTests.razor @@ -0,0 +1,51 @@ +@code { + + [Fact] + public void Page_ComponentRenders_WithoutError() + { + // Arrange + Services.AddScoped<IPageService, PageService>(); + + // Act + var cut = Render(@<Page />); + + // Assert - Component should render without throwing + cut.ShouldNotBeNull(); + } + + [Fact] + public void Page_ComponentWithTitle_ServiceHasCorrectTitle() + { + // Arrange + var pageService = new PageService(); + pageService.Title = "Test Title"; + Services.AddScoped<IPageService>(_ => pageService); + + // Act + Render(@<Page />); + + // Assert - Title is maintained in service + pageService.Title.ShouldBe("Test Title"); + } + + [Fact] + public void Page_TitleChangedDynamically_ComponentUpdates() + { + // Arrange + var pageService = new PageService(); + Services.AddScoped<IPageService>(_ => pageService); + var cut = Render(@<Page />); + + // Act - Set title dynamically + pageService.Title = "Dynamic Title"; + + // Wait for the component to update using bUnit's WaitForState + cut.WaitForState(() => pageService.Title == "Dynamic Title", TimeSpan.FromSeconds(2)); + + // Assert + pageService.Title.ShouldBe("Dynamic Title"); + } + +} + + diff --git a/src/BlazorWebFormsComponents.Test/PageService/InjectionTests.razor b/src/BlazorWebFormsComponents.Test/PageService/InjectionTests.razor new file mode 100644 index 00000000..fb6954c1 --- /dev/null +++ b/src/BlazorWebFormsComponents.Test/PageService/InjectionTests.razor @@ -0,0 +1,37 @@ +@using Microsoft.AspNetCore.Components.Rendering + +@code { + + [Fact] + public void PageService_CanBeInjected_IntoComponent() + { + // Arrange & Act + Services.AddScoped<IPageService, PageService>(); + var cut = Render(@<TestComponent />); + + // Assert - Component renders without error when PageService is injected + cut.Markup.ShouldContain("Test"); + } + + @code { + public class TestComponent : ComponentBase + { + [Inject] + public IPageService PageService { get; set; } = null!; + + protected override void OnInitialized() + { + // Verify we can access the service + _ = PageService.Title; + } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.OpenElement(0, "div"); + builder.AddContent(1, "Test"); + builder.CloseElement(); + } + } + } + +} diff --git a/src/BlazorWebFormsComponents.Test/PageService/MetaTagTests.razor b/src/BlazorWebFormsComponents.Test/PageService/MetaTagTests.razor new file mode 100644 index 00000000..a073fcf0 --- /dev/null +++ b/src/BlazorWebFormsComponents.Test/PageService/MetaTagTests.razor @@ -0,0 +1,111 @@ +@code { + + [Fact] + public void PageService_DefaultMetaDescription_IsEmpty() + { + // Arrange + var pageService = new PageService(); + + // Assert + pageService.MetaDescription.ShouldBe(string.Empty); + } + + [Fact] + public void PageService_DefaultMetaKeywords_IsEmpty() + { + // Arrange + var pageService = new PageService(); + + // Assert + pageService.MetaKeywords.ShouldBe(string.Empty); + } + + [Fact] + public void PageService_SetMetaDescription_UpdatesValue() + { + // Arrange + var pageService = new PageService(); + + // Act + pageService.MetaDescription = "This is a test page description"; + + // Assert + pageService.MetaDescription.ShouldBe("This is a test page description"); + } + + [Fact] + public void PageService_SetMetaKeywords_UpdatesValue() + { + // Arrange + var pageService = new PageService(); + + // Act + pageService.MetaKeywords = "test, keywords, blazor"; + + // Assert + pageService.MetaKeywords.ShouldBe("test, keywords, blazor"); + } + + [Fact] + public void PageService_SetMetaDescription_RaisesEvent() + { + // Arrange + var pageService = new PageService(); + string? capturedDescription = null; + pageService.MetaDescriptionChanged += (sender, description) => capturedDescription = description; + + // Act + pageService.MetaDescription = "New Description"; + + // Assert + capturedDescription.ShouldBe("New Description"); + } + + [Fact] + public void PageService_SetMetaKeywords_RaisesEvent() + { + // Arrange + var pageService = new PageService(); + string? capturedKeywords = null; + pageService.MetaKeywordsChanged += (sender, keywords) => capturedKeywords = keywords; + + // Act + pageService.MetaKeywords = "keyword1, keyword2"; + + // Assert + capturedKeywords.ShouldBe("keyword1, keyword2"); + } + + [Fact] + public void PageService_SetSameMetaDescription_DoesNotRaiseEvent() + { + // Arrange + var pageService = new PageService(); + pageService.MetaDescription = "Initial Description"; + int eventCount = 0; + pageService.MetaDescriptionChanged += (sender, description) => eventCount++; + + // Act + pageService.MetaDescription = "Initial Description"; + + // Assert + eventCount.ShouldBe(0); + } + + [Fact] + public void PageService_SetSameMetaKeywords_DoesNotRaiseEvent() + { + // Arrange + var pageService = new PageService(); + pageService.MetaKeywords = "Initial Keywords"; + int eventCount = 0; + pageService.MetaKeywordsChanged += (sender, keywords) => eventCount++; + + // Act + pageService.MetaKeywords = "Initial Keywords"; + + // Assert + eventCount.ShouldBe(0); + } + +} diff --git a/src/BlazorWebFormsComponents/IPageService.cs b/src/BlazorWebFormsComponents/IPageService.cs new file mode 100644 index 00000000..6cdacc9d --- /dev/null +++ b/src/BlazorWebFormsComponents/IPageService.cs @@ -0,0 +1,46 @@ +using System; + +namespace BlazorWebFormsComponents; + +/// <summary> +/// Provides page-level services that mimic ASP.NET Web Forms Page object functionality. +/// This service enables programmatic access to page properties like Title, MetaDescription, +/// and MetaKeywords, similar to how the Page object worked in Web Forms. +/// </summary> +public interface IPageService +{ + /// <summary> + /// Gets or sets the title of the page, which appears in the browser's title bar or tab. + /// This is equivalent to Page.Title in Web Forms. + /// </summary> + string Title { get; set; } + + /// <summary> + /// Gets or sets the meta description for the page. + /// This is equivalent to Page.MetaDescription in Web Forms (.NET 4.0+). + /// The description is used by search engines and appears in search results. + /// </summary> + string MetaDescription { get; set; } + + /// <summary> + /// Gets or sets the meta keywords for the page. + /// This is equivalent to Page.MetaKeywords in Web Forms (.NET 4.0+). + /// Keywords help categorize the page content for search engines. + /// </summary> + string MetaKeywords { get; set; } + + /// <summary> + /// Event raised when the Title property changes. + /// </summary> + event EventHandler<string>? TitleChanged; + + /// <summary> + /// Event raised when the MetaDescription property changes. + /// </summary> + event EventHandler<string>? MetaDescriptionChanged; + + /// <summary> + /// Event raised when the MetaKeywords property changes. + /// </summary> + event EventHandler<string>? MetaKeywordsChanged; +} diff --git a/src/BlazorWebFormsComponents/Page.razor b/src/BlazorWebFormsComponents/Page.razor new file mode 100644 index 00000000..4f1a6da0 --- /dev/null +++ b/src/BlazorWebFormsComponents/Page.razor @@ -0,0 +1,16 @@ +@inherits ComponentBase +@implements IDisposable +@if (!string.IsNullOrEmpty(_currentTitle)) +{ + <PageTitle>@_currentTitle</PageTitle> +} +<HeadContent> +@if (!string.IsNullOrEmpty(_currentMetaDescription)) +{ + <meta name="description" content="@_currentMetaDescription" /> +} +@if (!string.IsNullOrEmpty(_currentMetaKeywords)) +{ + <meta name="keywords" content="@_currentMetaKeywords" /> +} +</HeadContent> diff --git a/src/BlazorWebFormsComponents/Page.razor.cs b/src/BlazorWebFormsComponents/Page.razor.cs new file mode 100644 index 00000000..f129baeb --- /dev/null +++ b/src/BlazorWebFormsComponents/Page.razor.cs @@ -0,0 +1,78 @@ +using Microsoft.AspNetCore.Components; +using System; +using System.Threading.Tasks; + +namespace BlazorWebFormsComponents; + +/// <summary> +/// Component that provides Web Forms-style Page object functionality. +/// Use this component to set the page title and meta tags programmatically, +/// similar to Page.Title, Page.MetaDescription, and Page.MetaKeywords in Web Forms. +/// </summary> +public partial class Page : ComponentBase, IDisposable +{ + [Inject] + private IPageService PageService { get; set; } = null!; + + private string? _currentTitle; + private string? _currentMetaDescription; + private string? _currentMetaKeywords; + + protected override void OnInitialized() + { + PageService.TitleChanged += OnTitleChanged; + PageService.MetaDescriptionChanged += OnMetaDescriptionChanged; + PageService.MetaKeywordsChanged += OnMetaKeywordsChanged; + + _currentTitle = PageService.Title; + _currentMetaDescription = PageService.MetaDescription; + _currentMetaKeywords = PageService.MetaKeywords; + } + + private async void OnTitleChanged(object? sender, string newTitle) + { + try + { + _currentTitle = newTitle; + await InvokeAsync(StateHasChanged); + } + catch (ObjectDisposedException) + { + // Component was disposed before the state update completed. + // This is expected when navigating away while an event is in flight. + } + } + + private async void OnMetaDescriptionChanged(object? sender, string newMetaDescription) + { + try + { + _currentMetaDescription = newMetaDescription; + await InvokeAsync(StateHasChanged); + } + catch (ObjectDisposedException) + { + // Component was disposed before the state update completed. + } + } + + private async void OnMetaKeywordsChanged(object? sender, string newMetaKeywords) + { + try + { + _currentMetaKeywords = newMetaKeywords; + await InvokeAsync(StateHasChanged); + } + catch (ObjectDisposedException) + { + // Component was disposed before the state update completed. + } + } + + public void Dispose() + { + PageService.TitleChanged -= OnTitleChanged; + PageService.MetaDescriptionChanged -= OnMetaDescriptionChanged; + PageService.MetaKeywordsChanged -= OnMetaKeywordsChanged; + } +} diff --git a/src/BlazorWebFormsComponents/PageService.cs b/src/BlazorWebFormsComponents/PageService.cs new file mode 100644 index 00000000..c28aa2e3 --- /dev/null +++ b/src/BlazorWebFormsComponents/PageService.cs @@ -0,0 +1,82 @@ +using System; + +namespace BlazorWebFormsComponents; + +/// <summary> +/// Default implementation of IPageService that provides page-level services +/// mimicking ASP.NET Web Forms Page object functionality. +/// </summary> +public class PageService : IPageService +{ + private string _title = string.Empty; + private string _metaDescription = string.Empty; + private string _metaKeywords = string.Empty; + + /// <summary> + /// Gets or sets the title of the page, which appears in the browser's title bar or tab. + /// This is equivalent to Page.Title in Web Forms. + /// </summary> + public string Title + { + get => _title; + set + { + if (_title != value) + { + _title = value; + TitleChanged?.Invoke(this, _title); + } + } + } + + /// <summary> + /// Gets or sets the meta description for the page. + /// This is equivalent to Page.MetaDescription in Web Forms (.NET 4.0+). + /// The description is used by search engines and appears in search results. + /// </summary> + public string MetaDescription + { + get => _metaDescription; + set + { + if (_metaDescription != value) + { + _metaDescription = value; + MetaDescriptionChanged?.Invoke(this, _metaDescription); + } + } + } + + /// <summary> + /// Gets or sets the meta keywords for the page. + /// This is equivalent to Page.MetaKeywords in Web Forms (.NET 4.0+). + /// Keywords help categorize the page content for search engines. + /// </summary> + public string MetaKeywords + { + get => _metaKeywords; + set + { + if (_metaKeywords != value) + { + _metaKeywords = value; + MetaKeywordsChanged?.Invoke(this, _metaKeywords); + } + } + } + + /// <summary> + /// Event raised when the Title property changes. + /// </summary> + public event EventHandler<string>? TitleChanged; + + /// <summary> + /// Event raised when the MetaDescription property changes. + /// </summary> + public event EventHandler<string>? MetaDescriptionChanged; + + /// <summary> + /// Event raised when the MetaKeywords property changes. + /// </summary> + public event EventHandler<string>? MetaKeywordsChanged; +} diff --git a/src/BlazorWebFormsComponents/ServiceCollectionExtensions.cs b/src/BlazorWebFormsComponents/ServiceCollectionExtensions.cs index 03e82708..ee199e13 100644 --- a/src/BlazorWebFormsComponents/ServiceCollectionExtensions.cs +++ b/src/BlazorWebFormsComponents/ServiceCollectionExtensions.cs @@ -16,6 +16,7 @@ public static class ServiceCollectionExtensions public static IServiceCollection AddBlazorWebFormsComponents(this IServiceCollection services) { services.AddScoped<BlazorWebFormsJsInterop>(); + services.AddScoped<IPageService, PageService>(); return services; } }