Introduction
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.
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.
A component is a combination of:
For example, a "Rich Text Area" component might have:
TextBlob field for HTML contentRichTextArea class with a TextBlob propertyIn 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
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
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 |
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; }
}
@* 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; }
}
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>
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 ViewComponents are discovered automatically by convention. Just ensure:
TextBlockWithImage)ViewComponentInvoke or InvokeAsync methodSome components need to fetch additional data beyond what's in the module fields. For example, a "Posts Listing" component needs to fetch blog posts.
@* 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);
}
}
// 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);
}
}
Blazor supports interactive components that maintain state and respond to user actions via SignalR.
For components requiring interactivity, use a two-component pattern:
@* 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;
}
}
Each component should do one thing well. If a component becomes complex, break it into smaller sub-components.
Content fields can be null. Always use null-safe access:
// Good
@if (!string.IsNullOrEmpty(Fields?.Title))
// Bad - may throw NullReferenceException
@if (Fields.Title != "")
Handle missing content gracefully:
<h2>@(Fields?.Title ?? "Default Title")</h2>
<img src="@(Fields?.Image?.Url ?? "/images/placeholder.jpg")" />
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" />
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;