Building a Complete Page
This walkthrough builds a full Customer edit page step by step, covering route setup, graph definition, DataSet creation, layout with DataGraph and PropertyComponent, and conditional rendering.
1. Define the Route
A typical entity page has two routes — one for editing an existing entity by GUID and one for creating a new entity:
@page "/customers/{CustomerGuid:guid}"
@page "/customers/new"
When CustomerGuid is null (the /new route), DataGraph calls DataSet.CreateAsync() to create a new Customer on the server. When it has a value, DataGraph calls DataSet.LoadAsync(guid).
2. Define the Graph
The graph declares every property path the page will read or write. Define it as a static readonly field in the code-behind:
private static readonly GraphDefinition Graph = new GraphBuilder("Customer")
.Property("Id")
.Property("FullName")
.Reference("BillingCustomer")
.Navigation("PrimaryContact", pc => pc
.Property("CompanyName")
.Property("PrimaryEmail")
.Property("SecondaryEmail")
.Property("PrimaryPhone")
.Property("SecondaryPhone")
.Property("Website")
.Property("Note")
.Property("IsMailingAddressSameAsPhysical")
.Navigation("MailingAddress", ma => ma
.Property("Street")
.Property("City")
.Property("State")
.Property("PostalCode")
.Reference("Country"))
.Navigation("PhysicalAddress", pa => pa
.Property("Street")
.Property("City")
.Property("State")
.Property("PostalCode")
.Reference("Country")))
.Build();
See GRAPHS.md for details on the GraphBuilder API.
3. Create the DataSet
The DataSet connects the graph to the server via IODataClient. Create it in OnInitialized:
@inject IODataClient OData
private DataSet _dataSet = null!;
protected override void OnInitialized()
=> _dataSet = new DataSet(OData, Graph);
See DATASETS.md for the full DataSet API.
4. Build the Layout with DataGraph
DataGraph is the root container. It initializes metadata, loads or creates the entity, and cascades the editing context to all PropertyComponent children:
<DataGraph Definition="@Graph" DataSet="@_dataSet" EntityGuid="@CustomerGuid">
@* All PropertyComponent instances go here *@
</DataGraph>
DataGraph shows a loading spinner until the entity is initialized, then renders child content.
5. Add PropertyComponents
Place PropertyComponent instances inside DataGraph. Each one renders the right control based on the property's server metadata:
<DataGraph Definition="@Graph" DataSet="@_dataSet" EntityGuid="@CustomerGuid">
<MudStack Spacing="4">
<MudPaper Class="pa-4 rounded-lg" Elevation="2">
<MudText Typo="Typo.h6" Class="mb-4">Profile</MudText>
<MudGrid Spacing="3">
<MudItem xs="12" sm="6">
<PropertyComponent Path="Id" />
</MudItem>
<MudItem xs="12" sm="6">
<PropertyComponent Path="FullName" />
</MudItem>
<MudItem xs="12" sm="6">
<PropertyComponent Path="BillingCustomer" />
</MudItem>
<MudItem xs="12" sm="6">
<PropertyComponent Path="PrimaryContact.CompanyName" />
</MudItem>
</MudGrid>
</MudPaper>
</MudStack>
</DataGraph>
PropertyComponent determines automatically that:
Idis a text field (data typeIdText)FullNameis a read-only text field (virtual property)BillingCustomeris a reference → renders an entity selectorCompanyNameis a proper noun text field
See PROPERTY_COMPONENTS.md for all built-in mappings and customization options.
6. Organize into Sections
Use MudBlazor layout components to group related properties into visual sections:
@* Contact Section *@
<MudPaper Class="pa-4 rounded-lg" Elevation="2">
<MudText Typo="Typo.h6" Class="mb-4">Contact</MudText>
<MudGrid Spacing="3">
<MudItem xs="12" sm="6">
<PropertyComponent Path="PrimaryContact.PrimaryEmail" />
</MudItem>
<MudItem xs="12" sm="6">
<PropertyComponent Path="PrimaryContact.SecondaryEmail" />
</MudItem>
<MudItem xs="12" sm="6">
<PropertyComponent Path="PrimaryContact.PrimaryPhone" />
</MudItem>
<MudItem xs="12" sm="6">
<PropertyComponent Path="PrimaryContact.SecondaryPhone" />
</MudItem>
<MudItem xs="12">
<PropertyComponent Path="PrimaryContact.Note" />
</MudItem>
</MudGrid>
</MudPaper>
The xs and sm attributes control responsive layout — 6 of 12 columns means two fields per row on small screens and above.
7. Conditional Rendering with DataSet.Changed
Some UI sections should show or hide based on property values. For example, the physical address should only appear when IsMailingAddressSameAsPhysical is false.
To react to property changes, subscribe to the DataSet.Changed event:
private void HandleDataSetChanged(object? sender, IDataSet.ChangedArgs args)
{
if (args.AffectsPath("PrimaryContact.IsMailingAddressSameAsPhysical"))
{
InvokeAsync(StateHasChanged);
}
}
In the template, subscribe to the event and conditionally render:
<MudPaper Class="pa-4 rounded-lg" Elevation="2">
<MudStack Row="true" AlignItems="AlignItems.Center"
Justify="Justify.SpaceBetween" Class="mb-4">
<MudText Typo="Typo.h6">Physical Address</MudText>
<PropertyComponent Path="PrimaryContact.IsMailingAddressSameAsPhysical" />
</MudStack>
@if (!_dataSet.Get<bool>("PrimaryContact.IsMailingAddressSameAsPhysical"))
{
<MudGrid Spacing="3">
<MudItem xs="12">
<PropertyComponent Path="PrimaryContact.PhysicalAddress.Street" />
</MudItem>
<MudItem xs="12" sm="6">
<PropertyComponent Path="PrimaryContact.PhysicalAddress.City" />
</MudItem>
<MudItem xs="12" sm="3">
<PropertyComponent Path="PrimaryContact.PhysicalAddress.State" />
</MudItem>
<MudItem xs="12" sm="3">
<PropertyComponent Path="PrimaryContact.PhysicalAddress.PostalCode" />
</MudItem>
<MudItem xs="12" sm="6">
<PropertyComponent Path="PrimaryContact.PhysicalAddress.Country" />
</MudItem>
</MudGrid>
}
</MudPaper>
When the user toggles the checkbox, SetAsync tells the server, the Changed event fires, HandleDataSetChanged triggers StateHasChanged, and the physical address fields appear or disappear.
Complete Example
CustomerPage.razor
@page "/customers/{CustomerGuid:guid}"
@page "/customers/new"
@using Benevia.Core.Blazor
@using Benevia.Core.Blazor.DynamicComponents
@using Benevia.Core.Client.DataSets
@using Benevia.Core.Client.GraphDefinitions
@using Benevia.Core.Client.GraphDefinitions.Builders
@using Benevia.Core.Client.OData
@using MudBlazor
@inject IODataClient OData
<DataGraph Definition="@Graph" DataSet="@_dataSet" EntityGuid="@CustomerGuid">
@{
_dataSet.Changed -= HandleDataSetChanged;
_dataSet.Changed += HandleDataSetChanged;
}
<MudStack Spacing="4">
@* Profile Section *@
<MudPaper Class="pa-4 rounded-lg" Elevation="2">
<MudText Typo="Typo.h6" Class="mb-4">Profile</MudText>
<MudGrid Spacing="3">
<MudItem xs="12" sm="6">
<PropertyComponent Path="Id" />
</MudItem>
<MudItem xs="12" sm="6">
<PropertyComponent Path="FullName" />
</MudItem>
<MudItem xs="12" sm="6">
<PropertyComponent Path="BillingCustomer" />
</MudItem>
<MudItem xs="12" sm="6">
<PropertyComponent Path="PrimaryContact.CompanyName" />
</MudItem>
<MudItem xs="12" sm="6">
<PropertyComponent Path="PrimaryContact.Website" />
</MudItem>
</MudGrid>
</MudPaper>
@* Contact Section *@
<MudPaper Class="pa-4 rounded-lg" Elevation="2">
<MudText Typo="Typo.h6" Class="mb-4">Contact</MudText>
<MudGrid Spacing="3">
<MudItem xs="12" sm="6">
<PropertyComponent Path="PrimaryContact.PrimaryEmail" />
</MudItem>
<MudItem xs="12" sm="6">
<PropertyComponent Path="PrimaryContact.SecondaryEmail" />
</MudItem>
<MudItem xs="12" sm="6">
<PropertyComponent Path="PrimaryContact.PrimaryPhone" />
</MudItem>
<MudItem xs="12" sm="6">
<PropertyComponent Path="PrimaryContact.SecondaryPhone" />
</MudItem>
<MudItem xs="12">
<PropertyComponent Path="PrimaryContact.Note" />
</MudItem>
</MudGrid>
</MudPaper>
@* Mailing Address Section *@
<MudPaper Class="pa-4 rounded-lg" Elevation="2">
<MudText Typo="Typo.h6" Class="mb-4">Mailing Address</MudText>
<MudGrid Spacing="3">
<MudItem xs="12">
<PropertyComponent Path="PrimaryContact.MailingAddress.Street" />
</MudItem>
<MudItem xs="12" sm="6">
<PropertyComponent Path="PrimaryContact.MailingAddress.City" />
</MudItem>
<MudItem xs="12" sm="3">
<PropertyComponent Path="PrimaryContact.MailingAddress.State" />
</MudItem>
<MudItem xs="12" sm="3">
<PropertyComponent Path="PrimaryContact.MailingAddress.PostalCode" />
</MudItem>
<MudItem xs="12" sm="6">
<PropertyComponent Path="PrimaryContact.MailingAddress.Country" />
</MudItem>
</MudGrid>
</MudPaper>
@* Physical Address Section *@
<MudPaper Class="pa-4 rounded-lg" Elevation="2">
<MudStack Row="true" AlignItems="AlignItems.Center"
Justify="Justify.SpaceBetween" Class="mb-4">
<MudText Typo="Typo.h6">Physical Address</MudText>
<PropertyComponent Path="PrimaryContact.IsMailingAddressSameAsPhysical" />
</MudStack>
@if (!_dataSet.Get<bool>("PrimaryContact.IsMailingAddressSameAsPhysical"))
{
<MudGrid Spacing="3">
<MudItem xs="12">
<PropertyComponent Path="PrimaryContact.PhysicalAddress.Street" />
</MudItem>
<MudItem xs="12" sm="6">
<PropertyComponent Path="PrimaryContact.PhysicalAddress.City" />
</MudItem>
<MudItem xs="12" sm="3">
<PropertyComponent Path="PrimaryContact.PhysicalAddress.State" />
</MudItem>
<MudItem xs="12" sm="3">
<PropertyComponent Path="PrimaryContact.PhysicalAddress.PostalCode" />
</MudItem>
<MudItem xs="12" sm="6">
<PropertyComponent Path="PrimaryContact.PhysicalAddress.Country" />
</MudItem>
</MudGrid>
}
</MudPaper>
</MudStack>
</DataGraph>
CustomerPage.razor.cs
using Benevia.Core.Client.DataSets;
using Benevia.Core.Client.GraphDefinitions;
using Benevia.Core.Client.GraphDefinitions.Builders;
using Benevia.Core.Client.OData;
using Microsoft.AspNetCore.Components;
namespace MyApp.Pages.Customers;
public partial class CustomerPage : ComponentBase
{
[Parameter] public Guid? CustomerGuid { get; set; }
[Inject] public required IODataClient OData { get; set; }
private DataSet _dataSet = null!;
private static readonly GraphDefinition Graph = new GraphBuilder("Customer")
.Property("Id")
.Property("FullName")
.Reference("BillingCustomer")
.Navigation("PrimaryContact", pc => pc
.Property("CompanyName")
.Property("PrimaryEmail")
.Property("SecondaryEmail")
.Property("PrimaryPhone")
.Property("SecondaryPhone")
.Property("Website")
.Property("Note")
.Property("IsMailingAddressSameAsPhysical")
.Navigation("MailingAddress", ma => ma
.Property("Street")
.Property("City")
.Property("State")
.Property("PostalCode")
.Reference("Country"))
.Navigation("PhysicalAddress", pa => pa
.Property("Street")
.Property("City")
.Property("State")
.Property("PostalCode")
.Reference("Country")))
.Build();
protected override void OnInitialized()
=> _dataSet = new DataSet(OData, Graph);
private void HandleDataSetChanged(object? sender, IDataSet.ChangedArgs args)
{
if (args.AffectsPath("PrimaryContact.IsMailingAddressSameAsPhysical"))
{
InvokeAsync(StateHasChanged);
}
}
}
Key Patterns
View vs. Edit Mode
Pass DisplayMode to DataGraph to switch between editing and read-only display:
<DataGraph Definition="@Graph" DataSet="@_dataSet" EntityGuid="@guid"
DisplayMode="DisplayMode.View">
<PropertyComponent Path="Id" /> @* Renders as text *@
<PropertyComponent Path="PrimaryContact.PrimaryEmail" /> @* Renders as mailto link *@
</DataGraph>
Separate Graphs for View and Edit
If your view mode shows fewer properties, define a slimmer graph to reduce server load:
private static readonly GraphDefinition ViewGraph = new GraphBuilder("Customer")
.Property("Id")
.Property("FullName")
.Navigation("PrimaryContact", pc => pc
.Property("PrimaryEmail")
.Property("PrimaryPhone"))
.Build();
private static readonly GraphDefinition EditGraph = new GraphBuilder("Customer")
.Property("Id")
.Property("FullName")
.Reference("BillingCustomer")
.Navigation("PrimaryContact", pc => pc
.Property("CompanyName")
// ... all editable properties
)
.Build();
Reading Values Directly from the DataSet
For conditional logic or custom display, read values from the DataSet directly:
<DataGraph Definition="@Graph" DataSet="@_dataSet" EntityGuid="@CustomerGuid">
@{
var email = _dataSet.Get<string>("PrimaryContact.PrimaryEmail");
var phone = _dataSet.Get<string>("PrimaryContact.PrimaryPhone");
}
@if (!string.IsNullOrWhiteSpace(email))
{
<MudText>@email</MudText>
}
</DataGraph>
Lifecycle Summary
Page loads
├─ OnInitialized: Create DataSet(OData, Graph)
├─ DataGraph.OnParametersSetAsync:
│ ├─ Load EntityMetaData from IModelProvider
│ ├─ Create DataGraphContext (cascading)
│ ├─ LoadAsync(guid) or CreateAsync()
│ └─ Subscribe to DataSet.Changed
└─ PropertyComponent.OnParametersSet (per property):
├─ Resolve PropertyMetaData from path
├─ Resolve component type from DynamicComponentProvider
├─ Subscribe to DataSet.Changed for this path
└─ Render the resolved component