See Agility CMS in action. Watch a product demo
How Components Work in .NET
Components (previously called modules) are the building blocks of pages in Agility CMS. They represent functional, reusable pieces of content that editors can add to page zones.
This guide explains how to create and use components in your .NET application.
What is a Component?
A component is a combination of:
- Content Model (in Agility CMS) - Defines the fields editors can fill in
- C# Model (in your app) - Strongly-typed class matching the content model
- View/Template (in your app) - Renders the component's HTML
For example, a "Rich Text Area" component might have:
- Content Model: A single
TextBlobfield for HTML content - C# Model:
RichTextAreaclass with aTextBlobproperty - View: Renders the HTML content with proper styling
Component Architecture
Blazor Components
In Blazor, components are .razor files that receive data as parameters:
Components/
└── AgilityComponents/
├── RichTextArea.razor
├── Heading.razor
├── TextBlockWithImage.razor
└── PostsListing/
├── PostsListing.razor # Server component
└── PostsListingClient.razor # Interactive component
MVC ViewComponents
In MVC, components use the ViewComponent pattern with separate class and view:
ViewComponents/
└── PageModules/
├── RichTextArea.cs
├── Heading.cs
└── TextBlockWithImage.cs
Views/
└── PageModules/
├── RichTextArea.cshtml
├── Heading.cshtml
└── TextBlockWithImage.cshtml
Creating a Component
Step 1: Define the Content Model
In Agility CMS, create a Module Definition with the fields your component needs. For example, a "Text Block with Image" component might have:
| Field | Type | Description |
|---|---|---|
| Title | Text | Heading text |
| Content | HTML | Rich text content |
| Image | Image | Featured image |
| PrimaryButton | Link | Call-to-action button |
Step 2: Create the C# Model
Define a class matching your content model fields:
// Models/AgilityModels.cs
public class TextBlockWithImage
{
public string? Title { get; set; }
public string? Content { get; set; }
public ImageAttachment? Image { get; set; }
public Link? PrimaryButton { get; set; }
}
public class ImageAttachment
{
public string? Url { get; set; }
public string? Label { get; set; }
public int? Height { get; set; }
public int? Width { get; set; }
}
public class Link
{
public string? Href { get; set; }
public string? Target { get; set; }
public string? Text { get; set; }
}
Step 3: Create the View
Blazor Implementation
@* Components/AgilityComponents/TextBlockWithImage.razor *@
<section class="py-16">
<div class="container mx-auto px-4">
<div class="flex flex-wrap items-center">
@if (Fields?.Image?.Url != null)
{
<div class="w-full lg:w-1/2 mb-8 lg:mb-0">
<AgilityPic Image="@Fields.Image" FallbackWidth="600">
<AgilitySource Media="(min-width: 1024px)" Width="600" />
<AgilitySource Width="400" />
</AgilityPic>
</div>
}
<div class="w-full lg:w-1/2 lg:pl-12">
@if (!string.IsNullOrEmpty(Fields?.Title))
{
<h2 class="text-3xl font-bold mb-4">@Fields.Title</h2>
}
@if (!string.IsNullOrEmpty(Fields?.Content))
{
<div class="prose max-w-none">
@((MarkupString)Fields.Content)
</div>
}
@if (Fields?.PrimaryButton?.Href != null)
{
<a href="@Fields.PrimaryButton.Href"
target="@(Fields.PrimaryButton.Target ?? "_self")"
class="inline-block mt-6 px-6 py-3 bg-primary-500 text-white rounded">
@(Fields.PrimaryButton.Text ?? "Learn More")
</a>
}
</div>
</div>
</div>
</section>
@code {
[Parameter] public TextBlockWithImage? Fields { get; set; }
[Parameter] public dynamic? CustomData { get; set; }
}
MVC Implementation
ViewComponent:
// ViewComponents/PageModules/TextBlockWithImage.cs
public class TextBlockWithImage : ViewComponent
{
public IViewComponentResult Invoke(ModuleModel moduleModel)
{
var fields = moduleModel.Model.Fields.ToObject<TextBlockWithImageFields>();
return View("/Views/PageModules/TextBlockWithImage.cshtml", fields);
}
}
View:
@* Views/PageModules/TextBlockWithImage.cshtml *@
@model TextBlockWithImageFields
<section class="py-16">
<div class="container mx-auto px-4">
<div class="flex flex-wrap items-center">
@if (Model?.Image?.Url != null)
{
<div class="w-full lg:w-1/2 mb-8 lg:mb-0">
<img src="@Model.Image.Url?w=600&format=auto"
alt="@Model.Image.Label"
loading="lazy" />
</div>
}
<div class="w-full lg:w-1/2 lg:pl-12">
@if (!string.IsNullOrEmpty(Model?.Title))
{
<h2 class="text-3xl font-bold mb-4">@Model.Title</h2>
}
@if (!string.IsNullOrEmpty(Model?.Content))
{
<div class="prose max-w-none">
@Html.Raw(Model.Content)
</div>
}
@if (Model?.PrimaryButton?.Href != null)
{
<a href="@Model.PrimaryButton.Href"
target="@(Model.PrimaryButton.Target ?? "_self")"
class="inline-block mt-6 px-6 py-3 bg-primary-500 text-white rounded">
@(Model.PrimaryButton.Text ?? "Learn More")
</a>
}
</div>
</div>
</div>
</section>
Step 4: Register the Component
Blazor Registration
Add the component to the switch statement in AgilityComponent.razor:
@* Components/Shared/AgilityComponent.razor *@
@switch (ComponentName)
{
case "RichTextArea":
<RichTextArea Fields="@GetFields<RichTextArea>()" />
break;
case "Heading":
<Heading Fields="@GetFields<Heading>()" />
break;
case "TextBlockWithImage":
<TextBlockWithImage Fields="@GetFields<TextBlockWithImage>()" />
break;
// Add your new component here
case "MyNewComponent":
<MyNewComponent Fields="@GetFields<MyNewComponent>()" />
break;
default:
<div class="bg-red-100 p-4 text-red-700">
Unknown component: @ComponentName
</div>
break;
}
MVC Registration
MVC ViewComponents are discovered automatically by convention. Just ensure:
- The class name matches the module name (e.g.,
TextBlockWithImage) - The class inherits from
ViewComponent - It has an
InvokeorInvokeAsyncmethod
Components with External Data
Some components need to fetch additional data beyond what's in the module fields. For example, a "Posts Listing" component needs to fetch blog posts.
Blazor Example
@* PostsListing.razor *@
@inject IAgilityService AgilityService
@if (posts != null)
{
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
@foreach (var post in posts)
{
<article class="bg-white rounded shadow p-4">
<h3>@post.Title</h3>
<p>@post.Excerpt</p>
</article>
}
</div>
}
@code {
[Parameter] public PostsListing? Fields { get; set; }
private List<Post>? posts;
protected override async Task OnInitializedAsync()
{
posts = await AgilityService.GetPostsGraphQLAsync("en-us", take: 10);
}
}
MVC Example
// ViewComponents/PageModules/PostsListing.cs
public class PostsListing : ViewComponent
{
private readonly FetchApiService _fetchApi;
public PostsListing(FetchApiService fetchApi)
{
_fetchApi = fetchApi;
}
public async Task<IViewComponentResult> InvokeAsync(ModuleModel moduleModel)
{
var posts = await _fetchApi.GetContentByGraphQL<Post>(
query: @"{ posts { contentID fields { title slug excerpt } } }",
objName: "posts",
locale: moduleModel.Locale
);
return View("/Views/PageModules/PostsListing.cshtml", posts);
}
}
Interactive Components (Blazor Only)
Blazor supports interactive components that maintain state and respond to user actions via SignalR.
Server/Client Pattern
For components requiring interactivity, use a two-component pattern:
- Server Component - Fetches initial data during SSR
- Client Component - Handles interactive features
@* PostsListing.razor (Server) *@
@inject IAgilityService AgilityService
<PostsListingClient InitialPosts="@posts" />
@code {
private List<Post>? posts;
protected override async Task OnInitializedAsync()
{
posts = await AgilityService.GetPostsGraphQLAsync("en-us", take: 10);
}
}
@* PostsListingClient.razor (Client) *@
@rendermode @(new InteractiveServerRenderMode(prerender: true))
<div class="posts-grid">
@foreach (var post in allPosts)
{
<PostCard Post="@post" />
}
</div>
@if (hasMore)
{
<button @onclick="LoadMore" class="btn">Load More</button>
}
@code {
[Parameter] public List<Post> InitialPosts { get; set; } = new();
private List<Post> allPosts = new();
private bool hasMore = true;
protected override void OnInitialized()
{
allPosts = InitialPosts.ToList();
}
private async Task LoadMore()
{
var morePosts = await AgilityService.GetPostsGraphQLAsync(
"en-us",
take: 10,
skip: allPosts.Count
);
allPosts.AddRange(morePosts);
hasMore = morePosts.Count == 10;
}
}
Best Practices
1. Keep Components Focused
Each component should do one thing well. If a component becomes complex, break it into smaller sub-components.
2. Use Null-Conditional Operators
Content fields can be null. Always use null-safe access:
// Good
@if (!string.IsNullOrEmpty(Fields?.Title))
// Bad - may throw NullReferenceException
@if (Fields.Title != "")
3. Provide Fallbacks
Handle missing content gracefully:
<h2>@(Fields?.Title ?? "Default Title")</h2>
<img src="@(Fields?.Image?.Url ?? "/images/placeholder.jpg")" />
4. Use Responsive Images
Leverage Agility's image API for optimized images:
@* Blazor *@
<AgilityPic Image="@Fields.Image" FallbackWidth="800">
<AgilitySource Media="(min-width: 1024px)" Width="1200" />
<AgilitySource Media="(min-width: 768px)" Width="800" />
<AgilitySource Width="400" />
</AgilityPic>
@* MVC *@
<img src="@Fields.Image.Url?w=800&format=auto" />
5. Add Error Boundaries
Handle unknown components gracefully:
default:
<div class="p-4 bg-yellow-100 text-yellow-800 rounded">
<p><strong>Unknown component:</strong> @ComponentName</p>
<p>Create a component for this module type.</p>
</div>
break;
Next Steps
- How Page Models Work - Define where components can be placed
- Fetching Content - Load content for your components
- Blazor Starter - Blazor-specific component features
- MVC Starter - MVC-specific ViewComponent patterns