sitefinity-widget-expert

star 2

Use this skill when developing, reviewing, or troubleshooting Sitefinity MVC widgets on .NET Framework 4.8. Covers controller structure, the complete autogenerated designer attribute reference (field types, sections, conditional visibility, content selectors, LinkModel, TableView, choices), how property persistence actually works, view conventions, script/CSS loading, and custom designer views.

sitefinitysteve By sitefinitysteve schedule Updated 6/11/2026

name: sitefinity-widget-expert description: Use this skill when developing, reviewing, or troubleshooting Sitefinity MVC widgets on .NET Framework 4.8. Covers controller structure, the complete autogenerated designer attribute reference (field types, sections, conditional visibility, content selectors, LinkModel, TableView, choices), how property persistence actually works, view conventions, script/CSS loading, and custom designer views.

You are a Sitefinity MVC widget development expert targeting .NET Framework 4.8 (Feather-era widgets, MvcControllerProxy). The autogenerated-designer attributes below require Progress.Sitefinity.Renderer.dll (ships with Sitefinity 14.3+) referenced by the widgets project; the same attribute set is shared with the ASP.NET Core renderer, so the reference largely applies there too. Persistence behavior was verified against Sitefinity 15.4 platform behavior and a production database - see the companion skills sitefinity-database-structure (how widget rows are stored) and sitefinity-page-surgery (changing widgets programmatically).

Version baseline: Sitefinity 15.4 - attribute availability grows release to release, so on older versions verify before using. Check the target project's version: (Get-Item "<site>\bin\Telerik.Sitefinity.dll").VersionInfo.FileVersion (e.g. 15.4.8630.0 = Sitefinity 15.4), and confirm a specific attribute exists in that project's Progress.Sitefinity.Renderer.dll if in doubt.

Controller Structure

Every widget controller requires these attributes:

[EnhanceViewEnginesAttribute]  // Required - Feather view discovery across assemblies
[ControllerToolboxItem(Name = "MyWidget", Title = "My Widget", SectionName = "ContentToolboxSection", CssClass = "sfListitemsIcn sfMvcIcn")]
public class MyWidgetController : Controller
  • [EnhanceViewEnginesAttribute] is mandatory for Sitefinity to find views
  • [ControllerToolboxItem] registers the widget in the page editor toolbox; discovery is automatic (no Global.asax registration)
  • CssClass should combine a specific icon class with sfMvcIcn (the blue MVC badge): sfListitemsIcn, sfVideoIcn, sfImageViewIcn, sfFormsIcn, sfNavigationIcn, sfNewsViewIcn, ...
  • Controllers inherit from System.Web.Mvc.Controller directly (no Sitefinity base class required)
  • Optional: [IndexRenderMode(IndexRenderModes.NoOutput)] - widget renders nothing during search indexing

ICustomWidgetVisualization

Show a "click to configure" placeholder in the page editor when unconfigured:

public class MyWidgetController : Controller, ICustomWidgetVisualization
{
    public bool IsEmpty => string.IsNullOrEmpty(this.Title);
    public string EmptyLinkText => "Click to configure widget";
}

Gotcha: [DefaultValue]-populated content properties defeat IsEmpty - the designer fills the property before the controller runs, so the placeholder never shows. Keep IsEmpty checks on properties WITHOUT [DefaultValue] or C# initializers.

Actions

public ActionResult Index()
{
    var model = new MyWidgetModel { Title = this.Title };
    return View("Default", model);
}

protected override void HandleUnknownAction(string actionName)
{
    View("Default", this.GetModel()).ExecuteResult(this.ControllerContext);
}

Design-mode checks (skip expensive work in the page editor): SystemManager.IsDesignMode, SystemManager.IsPreviewMode.

How widget properties ACTUALLY persist (verified platform behavior)

Public properties on the controller become designer fields. Understanding persistence prevents whole bug classes:

  1. Persistence is sparse via default-instance diffing. On save, Sitefinity compares each property against a freshly constructed new YourController() and writes a database row ONLY for values that differ. Consequences:
    • A freshly dropped widget persists almost nothing - unset properties resolve from your C# defaults at runtime.
    • Changing a property's C# default later retroactively changes every existing widget instance that was saved with the old default - no row was ever written for it, so they all silently pick up the new default. Treat shipped defaults as a contract; if you must change one, audit existing instances first.
    • The [DefaultValue] attribute is NOT the persistence baseline (it seeds the designer UI); the C# property initializer is what the diff runs against.
  2. Values are invariant-culture strings (TypeConverter.ConvertToInvariantString): "True", "42". Custom types need a TypeConverter that round-trips invariant strings.
  3. Complex types MUST be marked [PropertyPersistence(PersistAsJson = true)] (Telerik.Sitefinity.Modules.Pages.PropertyPersisters). With it, the whole object graph stores as one JSON value. Without it, each list item persists as its own database object with per-property rows, and a list item whose values all equal that item type's defaults produces ZERO rows and silently vanishes on save. This is the #1 "my widget property doesn't stick" bug.
using Telerik.Sitefinity.Modules.Pages.PropertyPersisters;

[PropertyPersistence(PersistAsJson = true)]
public List<FeatureItem> Features { get; set; } = new List<FeatureItem>();

[PropertyPersistence(PersistAsJson = true)]
public MixedContentContext HeroImage { get; set; }
  1. [Browsable(false)] hides a property from the designer AND from persistence - use for computed/derived properties.

Designer attribute namespaces (MVC / .NET Framework 4.8)

using System.ComponentModel;                                       // DisplayName, Description, DefaultValue, Browsable, Category, ReadOnly
using System.ComponentModel.DataAnnotations;                       // Required, Range, StringLength, DataType, EmailAddress, Url, RegularExpression
using Telerik.Sitefinity.Web.Services.PropertyEditor.Attributes;   // ConditionalVisibility
using Telerik.Sitefinity.Frontend.Mvc.Infrastructure.Controllers.Attributes; // ViewSelector, EnhanceViewEnginesAttribute
using Telerik.Sitefinity.Modules.Pages.PropertyPersisters;         // PropertyPersistence
using Progress.Sitefinity.Renderer.Designers;                      // KnownFieldTypes, PropertyCategory (string-constant classes)
using Progress.Sitefinity.Renderer.Designers.Attributes;           // ContentSection, TableView, Choice, Content, TaxonomyContent, MediaItem, Group, Mirror, Placeholder, KnownContentTypes, ...
using Progress.Sitefinity.Renderer.Entities;                       // NumericRange, FileTypes
using Progress.Sitefinity.Renderer.Entities.Content;               // MixedContentContext
using Progress.Sitefinity.Renderer.Models;                         // LinkModel

(Reflection-verified on Renderer 15.4: KnownContentTypes lives in Progress.Sitefinity.Renderer.Designers.Attributes, NOT in ...Entities.Content. ConditionalVisibilityAttribute exists in BOTH Telerik.Sitefinity.dll and Progress.Sitefinity.Renderer.dll - either using works in MVC.)

Field types (C# type -> designer UI)

C# Type Designer UI Notes
string Text input Default for unknown types too
bool Yes/No radios [DataType(customDataType: KnownFieldTypes.CheckBox)] for a checkbox
int / double / nullable Number input
DateTime / DateTime? Date-time picker [DateSettings(ShowTime = false)] for date-only
enum Dropdown [Description] on members customizes labels; [Browsable(false)] hides a member
[Flags] enum Multi-select checkboxes
LinkModel Link picker See LinkModel section
MixedContentContext Content item selector Requires [Content] / [TaxonomyContent]
Complex object Expandable inline section
IList<T> List builder Add [TableView] for table layout
IDictionary<K,V> Key-value editor

Display attributes

[DisplayName("Hero Heading")]                  // field label
[Description("Shown under the field")]        // helper text
[DescriptionExtended("Extended", InlineDescription = "Inline hint", InstructionalNotes = "Notes")]
[Placeholder("Enter heading...")]              // input placeholder text
[DefaultValue("Build something amazing")]      // initial value on widget drop (also bool/int/enum/ISO-8601 date strings)
[FallbackToDefaultValueWhenEmpty]              // re-apply DefaultValue if user clears the field
[Browsable(false)]                             // hide from designer + persistence
[ReadOnly(true)]                               // visible, not editable
[DisplaySettings(Hide = true)]                 // hide field (alternative); HideContent = true keeps the label
[Mirror(nameof(Title), null)]                  // auto-fill from another property; user can override
[Copy(Exclude = true)]                         // skip this property when the widget is duplicated
[Suffix("px")]                                 // unit label on Range fields
[ContentContainer]                             // marks an Html field as a content container
[DynamicLinksContainer]                        // complex object/list containing LinkModel members

Validation (standard DataAnnotations)

[Required], [StringLength(100, MinimumLength = 5)], [MinLength]/[MaxLength], [Range(1, 20)], [EmailAddress], [Url], [RegularExpression(@"^#[0-9A-Fa-f]{6}$")] - all with ErrorMessage support and {0}/{1} placeholders.

Sections, views, grouping

// Basic vs Advanced tab
[Category(PropertyCategory.Advanced)]          // PropertyCategory: Basic (default), Advanced, QuickEdit
public string CssClass { get; set; }

// Collapsible sections within a view (second arg = order within section)
[ContentSection("Content", 0)]
public string Heading { get; set; }
[ContentSection("Appearance", 0)]
public string BackgroundColor { get; set; }

// Section display order - on the CLASS
[SectionsOrder("Content", "Appearance", "Layout")]
public class MyWidgetController : Controller { ... }

// Visual checkbox grouping
[Group("Display Options")]
[DataType(customDataType: KnownFieldTypes.CheckBox)]
public bool ShowTitle { get; set; }

Conditional visibility

Show/hide a field based on another property's value - JSON condition string:

[ConditionalVisibility("{\"conditions\":[{\"fieldName\":\"Template\",\"operator\":\"Equals\",\"value\":\"Split\"}]}")]
public string BackgroundImageUrl { get; set; }

// Boolean trigger: value is the string "true"/"false"
[ConditionalVisibility("{\"conditions\":[{\"fieldName\":\"ShowSecondaryButton\",\"operator\":\"Equals\",\"value\":\"true\"}]}")]
public string SecondaryButtonText { get; set; }

MVC namespace: Telerik.Sitefinity.Web.Services.PropertyEditor.Attributes; ASP.NET Core: Progress.Sitefinity.Renderer.Designers.Attributes.

Choice fields

// Chip selector backed by int (JSON choices)
[DataType(customDataType: KnownFieldTypes.ChipChoice)]
[Choice("[{\"Title\":\"1 column\",\"Name\":\"1\",\"Value\":1,\"Icon\":null},{\"Title\":\"2 columns\",\"Name\":\"2\",\"Value\":2,\"Icon\":null}]")]
public int? ColumnCount { get; set; }

// Dropdown backed by int
[DataType(customDataType: KnownFieldTypes.Choices)]
[Choice("[{\"Title\":\"1 item\",\"Name\":\"1\",\"Value\":1,\"Icon\":null}]")]
public int? ItemsPerRow { get; set; }

// Enum as radios / chips
[DataType(customDataType: KnownFieldTypes.RadioChoice)]
public TextAlignment Alignment { get; set; }

[Choice] extras: ServiceUrl (fetch options from an endpoint at design time), ServiceCallParameters, ServiceWarningMessage, ShowFriendlyName, SideLabel, ChipMaxThreshold, NotResponsive. Enum members take [Description("Label")] or [EnumDisplayName("Label")], [Icon("columns")] (Font Awesome name without fa-), and [Browsable(false)] to hide.

Content selectors (MixedContentContext)

Always pair with [PropertyPersistence(PersistAsJson = true)]:

[Content(Type = KnownContentTypes.Images, AllowMultipleItemsSelection = false)]
[PropertyPersistence(PersistAsJson = true)]
public MixedContentContext HeroImage { get; set; }

[Content(Type = "Telerik.Sitefinity.DynamicTypes.Model.MyModule.MyType")]   // Module Builder types by full name
[PropertyPersistence(PersistAsJson = true)]
public MixedContentContext FeaturedItems { get; set; }

[TaxonomyContent(Type = KnownContentTypes.Categories)]   // or a custom taxonomy name string
[PropertyPersistence(PersistAsJson = true)]
public MixedContentContext Categories { get; set; }

[Content] options (reflection-verified): Type, AllowMultipleItemsSelection (default true), AllowCreate (default true), DisableInteraction, ShowSiteSelector, LiveData (published only), Provider, plus IsFilterable, OpenMultipleItemsSelection, ForceShouldShowAll, TypeBlacklist, and the ManualSelection* family (MainFieldName, TabTitle, IconClass, BreadcrumbText, EntityType).

Resolving selections in the controller - the pattern is always ManagerBase.GetItems:

using Telerik.Sitefinity.Data;
using Telerik.Sitefinity.GenericContent.Model;

var images = ManagerBase.GetItems(this.HeroImage, KnownContentTypes.Images)
    .OfType<Telerik.Sitefinity.Libraries.Model.Image>()
    .Where(i => i.Status == ContentLifecycleStatus.Live)
    .ToList();
// Image: MediaUrl, AlternativeText, Title, Width, Height, Extension, MimeType, TotalSize, ResolveThumbnailUrl("profile")
// Document: MediaUrl, Title, Extension, TotalSize, MimeType
// DynamicContent: filter .Where(dc => dc.Status == ContentLifecycleStatus.Live && dc.Visible)

KnownContentTypes constants cover Pages, Images, Videos, Documents, News, Events, BlogPost/Blogs, Forms, Albums, Tags, Categories, GenericContent, Lists/ListItems, Calendars, DocumentLibraries, VideoLibraries, Folders, PageTemplates, Sites, Taxonomies.

Links: LinkModel (always, instead of text+url string pairs)

Progress.Sitefinity.Renderer.Models.LinkModel gives editors a proper picker (page selection, target, tooltip, anchor) in ONE field. Properties: Href, Text, Target, Tooltip, Sfref, Id, Type, QueryParams, Anchor, ClassList, Attributes.

[DataType(customDataType: "linkSelector")]   // enhanced picker UI (14.4+); omit for the basic one
public LinkModel PrimaryButton { get; set; }

// Reading (it's null until configured):
var text = this.PrimaryButton?.Text ?? "Learn more";
var href = this.PrimaryButton?.Href ?? "#";
var newTab = this.PrimaryButton?.Target == "_blank";

LinkModel[] works for multiple links; [Required] works on it.

Lists of complex objects: [TableView]

public class FeatureItem
{
    [ContentSection(1)]                 // 1-BASED column ordering - 0 means "unpositioned"!
    [DisplayName("Title")]
    public string Title { get; set; }

    [ContentSection(2)]
    [DisplayName("Description")]
    public string Description { get; set; }
}

[TableView(Reorderable = true, ColumnCount = 1)]    // 1 visible column; rest behind the pencil "More details" editor
[PropertyPersistence(PersistAsJson = true)]
public IList<FeatureItem> Features { get; set; } = new List<FeatureItem>();

[TableView] options (reflection-verified): ColumnTitle (or ctor arg), Enabled, Reorderable, Selectable, MultipleSelect, AllowEmptyState, AddManyFileName, ColumnCount (0 = all), HideRowsIfEmpty. [ContentSection(N)] on sub-object properties is 1-based - 0 is treated as unpositioned and floats to the end. The pencil-icon popup editor is internal to the AdminApp's sf-editable-table; you get it for free with IList<T> + [TableView], but it is not reusable from custom field editors (those use SelectorService.openDialog() from the AdminApp SDK instead).

Complex sub-objects also support [InitialValue("Text", "Click me")] (seed sub-property values on row creation).

Special types and DataType values

[DataType(customDataType: KnownFieldTypes.Html)]      public string RichContent { get; set; }   // rich text editor
[DataType(customDataType: KnownFieldTypes.TextArea)]  public string LongText { get; set; }      // multi-line
[DataType(customDataType: KnownFieldTypes.Color)]     public string Background { get; set; }    // color picker
[DataType(customDataType: KnownFieldTypes.Password)]  public string ApiKey { get; set; }
[DataType(customDataType: KnownFieldTypes.Range)] [Suffix("px")] public NumericRange Padding { get; set; }
[DataType(customDataType: KnownFieldTypes.FileTypes)] public FileTypes AllowedFiles { get; set; }
[MediaItem("images", false, false)]                    public string HeroImageId { get; set; }   // raw media selector (string id)
[DateSettings(ShowTime = false)]                       public DateTime PublishDate { get; set; }

KnownFieldTypes string values are lowercase/camelCase ("html", "textArea", "choiceYesNo", "radioChoices", "chipchoice", "choices", "choiceMultiple", "multipleChoiceChip", "color", "range", "attributes", "fileTypes", "dropdownWithText") - use the constants; a wrong-cased raw string silently falls back to a plain text input. Lesser-known attributes: [Dialog(json)], [IsNullable(true)], [LengthDependsOn(...)], [MappedType(DataType = "sf-long-text")], [RangeLimitation(...)].

[ColorPalette] is ASP.NET Core renderer ONLY - reflection-verified ABSENT from every assembly in a Sitefinity 15.4 MVC site (it ships in the Progress.Sitefinity.AspNetCore packages, not Progress.Sitefinity.Renderer.dll). In MVC use [DataType(customDataType: KnownFieldTypes.Color)] for a free color picker, or a [Choice] list of brand colors for a constrained palette.

Template selection: [ViewSelector] and its limitation

[ViewSelector] renders a dropdown of available views - but it only discovers views in ResourcePackages theme folders (ResourcePackages/{Theme}/MVC/Views/{Widget}/). If your widget views live in the project's Mvc/Views/ folder, the dropdown is EMPTY. The reliable alternative is an enum:

public enum MyWidgetTemplate
{
    [Description("Centered")] Centered,
    [Description("Split (Text + Image)")] Split
}

[DisplayName("Template")]
[DefaultValue(MyWidgetTemplate.Centered)]
public MyWidgetTemplate Template { get; set; } = MyWidgetTemplate.Centered;

[Browsable(false)]
public string TemplateName => $"MyWidget.{this.Template}";   // derived, hidden, not persisted

public ActionResult Index() => View(this.TemplateName, this.GetModel());

View conventions

Mvc/Views/MyWidget/
    Default.cshtml               (main view; alternates: MyWidget.Split.cshtml etc.)
    DesignerView.Simple.cshtml   (custom designer dialog, optional)
    Resources/
        widget.js
        widget.css
@model My.Namespace.Mvc.Models.MyWidgetModel
@using Telerik.Sitefinity.Frontend.Mvc.Helpers;   @* required for Html.Script/StyleSheet *@

@Html.Script("/Mvc/Views/MyWidget/Resources/widget.js?v=1.0", "bottom")   @* dedupes; "bottom"|"head"; ?v= for cache busting *@
@Html.StyleSheet("/Mvc/Views/MyWidget/Resources/widget.css", "head")
@Html.Partial("~/Mvc/Views/MyWidget/SharedScripts.cshtml")

Models are plain POCOs - no base class.

Custom designer views (DesignerView.Simple.cshtml, AngularJS)

When autogenerated fields aren't enough:

@model System.Web.UI.Control

<label>Title</label>
<input type="text" ng-model="properties.Title.PropertyValue" class="form-control" />

@* Booleans are STRING "True"/"False" in the property bag *@
<input type="checkbox" ng-model="properties.ShowDetails.PropertyValue"
       ng-true-value="'True'" ng-false-value="'False'"
       ng-checked="properties.ShowDetails.PropertyValue === 'True'" />

<select ng-model="properties.TemplateName.PropertyValue" class="form-control">
    <option value="Default">Default</option>
</select>

<sf-image-field sf-model="properties.ImageId.PropertyValue" />
<sf-html-field sf-model="properties.Instructions.PropertyValue" class="kendo-content-block" />
<textarea sf-code-area sf-type="htmlmixed" sf-model="properties.HtmlContent.PropertyValue"></textarea>
<expander expander-title='@Html.Resource("MoreOptions")'>...</expander>
<uib-tabset class="nav-tabs-wrapper"><uib-tab heading="Content">...</uib-tab></uib-tabset>

The "True"/"False" string convention is the invariant-string persistence showing through - everything in properties.*.PropertyValue is a string.

Documentation links

Topic URL
Designer overview https://www.progress.com/documentation/sitefinity-cms/autogenerated-widget-property-editors-for-asp.net-core
Field types https://www.progress.com/documentation/sitefinity-cms/autogenerated-field-types
Customize fields https://www.progress.com/documentation/sitefinity-cms/customize-autogenerated-fields
Complex objects https://www.progress.com/documentation/sitefinity-cms/autogenerated-complex-objects
Validations https://www.progress.com/documentation/sitefinity-cms/field-validations
Conditional fields https://www.progress.com/documentation/sitefinity-cms/conditional-fields
Sections & views https://www.progress.com/documentation/sitefinity-cms/sections-and-views
All-properties sample https://www.progress.com/documentation/sitefinity-cms/sample--all-properties-widget-designer
GitHub samples https://github.com/Sitefinity/sitefinity-aspnetcore-mvc-samples
Install via CLI
npx skills add https://github.com/sitefinitysteve/SitefinityCommunity.Mcp --skill sitefinity-widget-expert
Repository Details
star Stars 2
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator
sitefinitysteve
sitefinitysteve Explore all skills →