Extending ViewModels
A ViewModel is one of the ways you can pass information from a DynamicWeb 10 solution to the frontend - read about templating in DynamicWeb here. ViewModels are fast in part because they only contain the properties you need in a context, but if you need access to non-standard information on a ViewModel you can extend our existing ViewModels with new properties.
When you need to extend a ViewModel, you first need to identify which ViewModel you want to extend.
- Here is a list of all the ViewModels which inherit from ViewModelBase
- Here is a list of the ViewModels inheriting from FillableViewModelBase
When you know which ViewModel you want to extend, you can open your favorite IDE and start implementing.
Subclassing vs Extension Methods
When you need to add custom functionality to a ViewModel, you have two main approaches:
- Writing C# extension methods against the ViewModel type
- Subclassing the ViewModel (i.e. creating a new class that inherits from the existing ViewModel)
Both approaches work, but each has its own trade-offs:
Aspect | Subclassing ViewModels | Extension Methods |
---|---|---|
Performance | • Minimal overhead: instantiation cost is virtually the same as the base ViewModel. • New properties are part of the object graph, so no extra runtime lookup. |
• No extra object creation—methods are static. • Each call incurs the small cost of a static method invocation. |
Strong typing | • New properties appear directly on the type, enabling IntelliSense and compile-time checking. | • Methods show up under “Extensions” in IntelliSense; properties must be accessed via method syntax. |
Discoverability | • All custom members live alongside inherited ones in the class definition. • Easier for new team members to see what data is available. |
• Helpers are located in a static class—developers must know to look there. |
Separation of concerns | • Combines data and logic in one class. Over-subclassing can bloat ViewModels. | • Keeps augmentation logic completely separate from the ViewModel itself. |
Reusability | • Tied to a specific ViewModel type. If you want the same logic in another ViewModel, you must repeat or refactor it. | • Can target any type (or interface) globally, making the same helper available everywhere. |
Dependency injection | • Can constructor-inject services into your subclass if you register it in DI. | • Cannot inject services directly into static extension methods; would need to resolve them manually. |
Compatibility | • If the original ViewModel implementation changes (e.g. method signature), your subclass may need updating. | • Extension methods are resilience-based: as long as the public API remains, they continue to compile. |
Maintainability | • Over time, many small subclasses can grow hard to manage. • Risks of conflicting property names if multiple add-ins are in play. |
• Centralized helper classes are easier to version and unit-test in isolation. |
You should use subclassing when...
- ...you need new properties that you want to consume directly in your Razor templates
- ...you require constructor injection of services (e.g. logging, data fetchers)
- ...you want the custom members to participate in JSON serialization or model binding automatically
You should use extensions methods when...
- ...you’re adding simple helpers or computed values that don’t need to be serialized
- ...you prefer to keep your domain model classes lean and push utility logic into static helpers
- ...you want the same functionality available across multiple ViewModel types without inheritance
Ultimately, choose the approach that best balances performance, discoverability and maintainability for your project. In many cases you may even combine both: use extension methods for purely presentation-focused helpers, and subclassing when you need rich, injectable, serializable data on the ViewModel itself.
Extending ViewModels using extension methods
If you prefer to keep your ViewModel classes lean and push additional logic into static helpers, C# extension methods are an excellent choice. Below is an example showing how you can add a presentation-focused helper to the existing PageInfoViewModel
without creating a new subclass. In this case, we’ll add a method that returns the full breadcrumb path for the current page.
using Dynamicweb.Rendering.ViewModels;
using System;
using System.Collections.Generic;
using System.Linq;
namespace MyProject.ViewModelExtensions
{
/// <summary>
/// Provides helper methods for PageInfoViewModel.
/// </summary>
public static class PageInfoViewModelExtensions
{
/// <summary>
/// Builds a readable breadcrumb trail by combining each ancestor’s
/// title and URL into a single string.
/// </summary>
/// <param name="model">The PageInfoViewModel instance.</param>
/// <param name="separator">Separator string, e.g. " > ".</param>
/// <returns>A formatted breadcrumb string.</returns>
public static string ToBreadcrumbs(this PageInfoViewModel model, string separator = " > ")
{
if (model == null)
throw new ArgumentNullException(nameof(model));
// Assume model.Ancestors is IEnumerable<PageInfoViewModel>
var segments = model
.Ancestors
.Select(a => $"{a.Title}")
.Concat(new[] { model.Title });
return string.Join(separator, segments);
}
}
}
With this extension in place, you can call:
@model PageInfoViewModel
<p>@Model.ToBreadcrumbs()</p>
…to render a breadcrumb trail like:
Home > Products > Electronics > Cameras
This keeps your core ViewModel untouched, centralizes presentation logic, and makes the helper available anywhere you have a PageInfoViewModel
.
Extending ViewModels using subclassing
To start extending an existing ViewModel, you need to create a new class, that inherits from the ViewModel, you want to extend. In the new class, you can override an existing method or property by using the new
keyword or add completely new properties and methods, which can return the data you need.
You can read about C# Extension Methods in general here.
It's not possible to change the original values on the ViewModel, because they will be overwritten as part of the ViewModel instantiation, so if you need to return some different data in a property, you need to create your own property in your class, and have that return the correct data.
using Dynamicweb.Frontend;
namespace ViewModelExtensibility
{
public class PageInfoViewModelExtended : PageInfoViewModel
{
private object _instance;
public PageInfoViewModelExtended()
{
//this is initialisation. The model does not have anyting set at this point.
//Initialize objects, i.e. services, that can be used throughtout the instance - i.e. to get addtional data.
_instance = new object();
//This will not work as the header will be overriden later in the initialisation process of this object.
base.Name = "Set name in constructor does not work";
}
/// <summary>
/// Take over default property of the base viewmodel. Suitable to change default behavior
/// </summary>
public new string Name
{
get
{
return $"{base.Name} changed1";
}
}
/// <summary>
/// Addtional data property available in template. Suitable for additional data
/// </summary>
public string MetaTitleOrName
{
get
{
//Retrive the actual page from the database to access information, which is not available on the PageViewModel
var page = Dynamicweb.Content.Services.Pages.GetPage(base.ID);
if (!string.IsNullOrEmpty(page.MetaTitle))
{
return page.MetaTitle;
}
else
{
return page.GetDisplayName();
}
}
}
}
}
Item-based ViewModels
In some cases you have ViewModels which are based on an Item. If you only want to extend a ViewModel working on a specific ItemType, then you can do that by adding the AddInName, with the SystemName of the ItemType, to the class.
using System;
using System.Collections.Generic;
using System.Text;
using Dynamicweb.Frontend;
using Dynamicweb.Rendering;
using Dynamicweb.Extensibility.AddIns;
namespace Dynamicweb.Examples.Rendering
{
/// <summary>
/// Custom paragraph viewmodel for specific item type. Use AddInName attribute with the item type name. See <see cref="AddInName"/>
/// </summary>
[AddInName("MultiPurposeParagraphInfo")]
public class SwiftPosterViewModel : ParagraphViewModel
{
private object _instance;
public SwiftPosterViewModel()
{
//this is initialisation. The model does not have anyting set at this point.
//Initialize objects, i.e. services, that can be used throughtout the instance - i.e. to get addtional data.
_instance = new object();
//This will not work as the header will be overriden later in the initialisation process of this object. Don't try to initialize any properties of the base class.
base.Header = "Seting the header inside the constructor does not work";
}
/// <summary>
/// Take over default property of the base viewmodel. Suitable to change default behavior
/// </summary>
public new string Header
{
get
{
return $"{base.Header} changed1";
}
}
/// <summary>
/// Addtional data property available in template. Suitable for additional data
/// </summary>
public string NameOrTile
{
get
{
if (Item.GetRawValue("Title") is object)
{
return Item.GetRawValue("Title").ToString();
}
else
{
return $"From nameOrTitle prop: {Header}";
}
}
}
/// <summary>
/// Method to call from template using data from the model instance. Suitable for rendering logic
/// </summary>
/// <returns></returns>
public string GetPosterPadding()
{
string posterPadding = Item.GetRawValueString("ContentPadding", string.Empty);
string posterPaddingClass = "px-3";
switch (posterPadding)
{
case "none":
posterPaddingClass = " p-3 px-xl-3 py-xl-4";
break;
case "small":
posterPaddingClass = " p-3 p-xl-4";
break;
case "large":
posterPaddingClass = " p-4 p-xl-5";
break;
}
return posterPaddingClass;
}
}
[AddInName("MultiPurposeParagraphInfo")]
public class SwiftPosterInfoViewModel : ParagraphInfoViewModel
{
/// <summary>
/// Take over default property of the base viewmodel. Suitable to change default behavior
/// </summary>
public new string Name
{
get
{
return $"{base.Name} changed2";
}
}
/// <summary>
/// Addtional data property available in template. Suitable for additional data
/// </summary>
public string NameOrTile
{
get
{
if (Item.GetRawValue("Title") is object)
{
return Item.GetRawValue("Title").ToString();
}
else
{
return $"From nameOrTitle prop: {Name}";
}
}
}
/// <summary>
/// Method to call from template using data from the model instance. Suitable for rendering logic
/// </summary>
/// <returns></returns>
public string GetPosterPadding()
{
string posterPadding = Item.GetRawValueString("ContentPadding", string.Empty);
string posterPaddingClass = "px-3";
switch (posterPadding)
{
case "none":
posterPaddingClass = " p-3 px-xl-3 py-xl-4";
break;
case "small":
posterPaddingClass = " p-3 p-xl-4";
break;
case "large":
posterPaddingClass = " p-4 p-xl-5";
break;
}
return posterPaddingClass;
}
}
}