Appearance
Are you an LLM? You can read better optimized documentation at /components/view_components.md for this page in Markdown format
View components
This document provides a detailed technical overview of the View Components used in TheExampleApp. View Components are self-contained, reusable rendering units that encapsulate server-side logic and presentation. They are a core part of the application's UI architecture, promoting modularity and testability.
View components overview
In ASP.NET Core, a View Component is similar to a partial view, but significantly more powerful. They are designed to handle rendering tasks that are too complex for a partial view but don't require the full lifecycle of a controller. Key characteristics include:
- Self-Contained: They bundle their own logic and view, making them independent and reusable across the application.
- Separation of Concerns: They don't rely on model binding and only work with data provided when they are invoked, enforcing a clear separation from the parent view's model.
- Testability: The logic is contained within a C# class that can be easily unit-tested in isolation, as demonstrated by the project's use of xUnit and Moq.
- Dependency Injection: They fully support constructor-based dependency injection, allowing them to access application services like the
IContentfulClient.
These components are a fundamental part of the application's server-side rendering strategy, which is built on ASP.NET Core 2.1 and the Razor view engine. For more context on the overall view architecture, see the Razor Pages documentation.
LocalesViewComponent
This component renders the language/locale switcher dropdown, allowing users to change the content language.
Implementation Details
The LocalesViewComponent is responsible for fetching the available locales from the Contentful space and determining the currently selected locale.
- Dependencies: It injects
IContentfulClientto communicate with the Contentful API. - Logic (
InvokeAsync):- It asynchronously calls
_client.GetSpace()to retrieve the space's configuration, which includes a list of all defined locales. - It reads the current locale from the user's session (
HttpContext.Session.GetString(Startup.LOCALE_KEY)). If no locale is set in the session, it defaults to the server's current culture string (CultureInfo.CurrentCulture.ToString()). - It determines the selected locale by finding the matching locale from the space's locales list, or falls back to the default locale if no match is found. This selected locale code is then saved back to the session to ensure persistence across requests.
- The component passes a
LocalesInfomodel to its view, containing the full list of locales, the currently selected locale, and the preview API status flag (UsePreviewApi).
- It asynchronously calls
Edge Cases and Error Handling
A crucial implementation detail is its resilience to API failures. If the call to _client.GetSpace() fails (e.g., due to invalid credentials or network issues), the component catches the exception and falls back to a default Space object containing only "en-US". This prevents the entire page from crashing if the Contentful management API is unreachable.
csharp
// TheExampleApp/ViewComponents/LocalesViewComponent.cs
public async Task<IViewComponentResult> InvokeAsync()
{
Space space = null;
try
{
// Try to get the space, if this fails we're probably in a state of bad credentials.
space = await _client.GetSpace();
}
catch
{
// Getting the space failed, set some default values.
space = new Space() {
Locales = new List<Locale>()
{
new Locale
{
Code = "en-US",
Default = true,
Name = "U.S. English"
}
}
};
}
// ... logic to determine selected locale ...
return View(localeInfo);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
For more information on how localization is handled throughout the application, refer to the Internationalization documentation.
EntryStateViewComponent
This component is a key part of the editorial experience. It displays the current publication state of a Contentful entry: Draft, Published, or Pending Changes.
Implementation Details
The component determines an entry's state by comparing its representation in the Preview API versus the Delivery API.
- Dependencies: It injects
HttpClientandIContentfulOptionsManagerto create a dedicated, short-lived Contentful client. - Logic (
InvokeAsync):- The component receives the
SystemPropertiesof one or more entries from the parent view, which are fetched using the Preview API. - Crucially, it instantiates a new
ContentfulClientthat is hardcoded to use the Delivery API (falsefor theusePreviewApiparameter). This is necessary to check the published status of an entry, regardless of the application's current API mode. - It queries the Delivery API for the entries using their IDs.
- State Logic:
- Draft: If the Delivery API call throws an exception, returns null, or returns results where any requested entry ID is missing from the response, it implies at least one entry is not published.
- Pending Changes: If entries exist in both APIs, the component compares their
UpdatedAttimestamps. A mismatch indicates that the preview version is newer than the published version. - Published: If entries are found and timestamps match, they are considered published with no pending changes.
- The component receives the
Technical Gotcha: Timestamp Precision
When comparing UpdatedAt timestamps, the component uses a TrimMilliseconds local function within InvokeAsync. This is critical because the timestamps returned by the API may have minor precision differences that could lead to incorrectly flagging an entry as having "Pending Changes". This function normalizes the DateTime objects by stripping milliseconds before comparison.
csharp
// TheExampleApp/ViewComponents/EntryStateViewComponent.cs
DateTime? TrimMilliseconds(DateTime? dateTime)
{
if (!dateTime.HasValue)
{
return dateTime;
}
return new DateTime(dateTime.Value.Year, dateTime.Value.Month, dateTime.Value.Day,
dateTime.Value.Hour, dateTime.Value.Minute, dateTime.Value.Second, 0);
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
This component is typically used within the EditorialFeaturesViewComponent to provide editors with immediate visual feedback and deep linking capabilities.
MetaTagsViewComponent
This simple but important component is responsible for generating the HTML <title> tag for each page, contributing to SEO and browser tab readability.
Implementation Details
It constructs the page title by combining a page-specific title with a global, localized application title.
- Dependencies: It injects
IViewLocalizerto access localized string resources. - Logic (
Invoke):- The component accepts an optional
titleparameter. - If a
titleis provided, it formats it as"{title} — {defaultTitle}", wheredefaultTitleis retrieved from the locale resource files (e.g.,en-US.json). - If no
titleis provided, it simply uses thedefaultTitle.
- The component accepts an optional
csharp
// TheExampleApp/ViewComponents/MetaTagsViewComponent.cs
public IViewComponentResult Invoke(string title)
{
if (string.IsNullOrEmpty(title))
{
return View("Default", _localizer["defaultTitle"].Value);
}
return View("Default", $"{title} — {_localizer["defaultTitle"].Value}");
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
This design ensures a consistent title format across the entire application while supporting internationalization.
SelectedApiViewComponent
This component renders the UI element (a dropdown) that allows developers and editors to switch between the Contentful Delivery API and Preview API.
Implementation Details
It determines the current API mode and passes this state, along with the current request path, to its view.
- Dependencies: It injects
IContentfulClient. - Logic (
Invoke):- It accepts a
currentPathstring, which represents the URL path of the current page. - It checks the
_client.IsPreviewClientproperty to determine if the application is currently in preview mode. - It passes a
Tuple<bool, string>to the view. Theboolindicates the preview status, and thestring(currentPath) is used by the view to construct a correct redirect URL when the user selects a different API, ensuring they stay on the same page.
- It accepts a
csharp
// TheExampleApp/ViewComponents/SelectedApiViewComponent.cs
public IViewComponentResult Invoke(string currentPath)
{
// The model is a tuple where Item1 is the preview status (bool)
// and Item2 is the current path (string).
return View(Tuple.Create(_client.IsPreviewClient, currentPath));
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
EditorialFeaturesViewComponent
This component acts as a container or a feature flag for all editorial-related UI elements, such as the entry state and deep links to the Contentful web app.
Implementation Details
It decides whether to render its child content based on a session flag. This allows the entire suite of editorial tools to be toggled on or off.
- Dependencies: It injects
IContentfulOptionsManagerto access Contentful configuration likeSpaceId. - Logic (
Invoke):- It checks the user's session for the key
"EditorialFeatures". The features are considered active only if the value is the string"Enabled". - It populates an
EditorialFeaturesModelwith the necessary data for its child components, including:- The
SystemPropertiesof the entry. - The
FeaturesEnabledboolean flag. - The current
SpaceIdandUsePreviewApistatus.
- The
- The view for this component will typically contain conditional logic to render other components (like
EntryStateViewComponent) only ifFeaturesEnabledis true.
- It checks the user's session for the key
csharp
// TheExampleApp/ViewComponents/EditorialFeaturesViewComponent.cs
public IViewComponentResult Invoke(IEnumerable<SystemProperties> sys)
{
var model = new EditorialFeaturesModel();
model.Sys = sys;
// Checks the session to see if the feature should be rendered.
model.FeaturesEnabled = HttpContext.Session.GetString("EditorialFeatures") == "Enabled";
model.SpaceId = _options.SpaceId;
model.UsePreviewApi = _options.UsePreviewApi;
return View(model);
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
This component is central to providing the enhanced editorial experience and deep linking.
Component invocation
View Components are invoked from within a Razor view (.cshtml file) using the Component.InvokeAsync or Component.Invoke helper methods. The name of the component to invoke is the class name without the "ViewComponent" suffix.
Asynchronous Invocation
For components with an InvokeAsync method, use the await keyword. This is the standard for components performing I/O operations.
razor
@* Invokes LocalesViewComponent.cs. No parameters are needed. *@
@await Component.InvokeAsync("Locales")1
2
2
Synchronous Invocation
For components with a synchronous Invoke method, the await keyword is not needed.
razor
@* Invokes MetaTagsViewComponent.cs, passing the title as an anonymous object. *@
@Component.Invoke("MetaTags", new { title = "About Us" })1
2
2
These calls are typically placed in shared layout files (see /views/layout.md) for components that appear on every page, or in specific views for page-local components.
Component models
Each View Component uses a strongly-typed model to pass data to its corresponding Razor view. This practice improves code clarity and reduces runtime errors. The following table summarizes the models used.
| View Component | Model Class | Description |
|---|---|---|
LocalesViewComponent | LocalesInfo | Contains the list of available locales from Contentful, the currently selected locale, and a flag indicating the API mode. |
EntryStateViewComponent | EntryStateModel | Holds the SystemProperties of an entry and boolean flags (Draft, PendingChanges) representing its publication state. |
EditorialFeaturesViewComponent | EditorialFeaturesModel | Carries the entry's SystemProperties, a flag to enable/disable features, the Contentful SpaceId, and the API mode. |
SelectedApiViewComponent | Tuple<bool, string> | A tuple where Item1 is a boolean for preview mode status and Item2 is the current URL path for redirection. |
MetaTagsViewComponent | string | A simple string representing the final, formatted page title to be rendered. |