Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .ai-team/agents/cyclops/history.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<input type="file">` 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 `<area>` 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
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ There are a handful of features that augment the ASP<span></span>.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
Expand Down
305 changes: 305 additions & 0 deletions docs/UtilityFeatures/PageService.md
Original file line number Diff line number Diff line change
@@ -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 `<title>` 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)
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions samples/AfterBlazorServerSide/Components/Layout/NavMenu.razor
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Loading
Loading