MVVM and Data Binding Patterns for the WPF HTML Editor

The WpfHtmlEditor is a composite control that hosts a WebBrowser, several toolbars, a raw-source TextBox, and a preview pane. Because of that, MVVM bindings work the way you expect them to for a handful of well-chosen dependency properties, but not for every public property on the control. This page walks through the supported patterns, the recurring traps ("FontName/FontSize not working in MVVM", "how do I track every change for autosave?"), and the workarounds for properties that are not DependencyProperty-backed.

Why MVVM with this editor is trickier than with a plain TextBox

A WPF Binding can only target a DependencyProperty. The editor exposes seven dependency properties intended for data binding (verified in PublicAPI.cs):

PropertyTypeDefault ModeDefault TriggerTypical Use
BodyHtmlstringTwoWayLostFocusThe HTML inside <body>. The property most ViewModels want to bind.
DocumentHtmlstringTwoWayLostFocusThe full document including <head>. Use when you need DOCTYPE / meta / style preserved.
DocumentTitlestringTwoWayLostFocusThe contents of <title>.
EditorModeEditorModesOneWayPropertyChangedSwitch WYSIWYG / Source / Preview from a ViewModel toggle.
LanguageEditorLanguageOneWayPropertyChangedSet the UI locale from a settings ViewModel.
Toolbar1ItemsSourceIEnumerableOneWayPropertyChangedInject extra toolbar items from a collection.
Toolbar2ItemsSourceIEnumerableOneWayPropertyChangedSame, for the second toolbar strip.

Everything else — Options, DefaultFontFamily, DefaultFontSizeInPt, DefaultForeColor, BaseUrl, SpellCheckOptions, LicenseKey, the Content / Formatting / Selection services — is a regular CLR property or method and cannot be data-bound directly. Trying to write FontName="{Binding ...}" in XAML silently produces a binding error in the Output window because FontName is not a property on the editor at all (the customer was thinking of the toolbar combo box).

One-way binding from ViewModel to editor

The simplest case: render whatever the ViewModel has into the editor and never push edits back. Use this for read-only previews of HTML produced elsewhere.

<wpfeditor:WpfHtmlEditor x:Name="MyEditor"
                         BodyHtml="{Binding HtmlContent, Mode=OneWay}" />

Two-way binding (the recommended pattern)

The three string DPs ship with BindsTwoWayByDefault = true and DefaultUpdateSourceTrigger = LostFocus, so the minimal XAML already writes back when the editor loses focus:

<wpfeditor:WpfHtmlEditor x:Name="MyEditor"
                         BodyHtml="{Binding HtmlContent}" />

For explicitness, spell out the mode and the trigger:

<wpfeditor:WpfHtmlEditor x:Name="MyEditor"
                         BodyHtml="{Binding HtmlContent,
                                            Mode=TwoWay,
                                            UpdateSourceTrigger=LostFocus}" />

Important: do not switch to UpdateSourceTrigger=PropertyChanged for BodyHtml. The editor fires HtmlChanged on every keystroke, paste, undo step, and toolbar action. With PropertyChanged the ViewModel setter is invoked on every one of those, which (a) thrashes the GC with new strings, and (b) commonly causes the customer's OnHtmlContentChanged handler to re-enter the editor (for example to re-format), which resets the caret. LostFocus writes one snapshot per editing session and is what every shipped sample uses.

Properties that are NOT dependency properties — the workarounds

When the property you want is on editor.Options or is a top-level CLR property (for example BaseUrl or DefaultFontFamily), you have two clean options:

Option A: a one-line code-behind projection

The view's code-behind is allowed to read the DataContext and copy values onto the editor. This is still MVVM-clean — the ViewModel does not reference any WPF type — because the projection lives in the view layer:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        DataContextChanged += (s, e) => ApplyVmToEditor();
    }

    private void ApplyVmToEditor()
    {
        if (DataContext is MainViewModel vm)
        {
            MyEditor.DefaultFontFamily   = vm.StandardFontName;
            MyEditor.DefaultFontSizeInPt = vm.StandardFontSize;
            MyEditor.Options.AutoDetectWordPaste = vm.CleanWordPaste;
            MyEditor.BaseUrl = vm.ImageBaseUrl;
        }
    }
}

Option B: an attached behavior

If you would rather keep the wiring in XAML, write a one-property attached behavior. The example below makes the editor's non-bindable BaseUrl reachable from a {Binding}:

using System.Windows;
using SpiceLogic.HtmlEditor.WPF;

public static class EditorBindingBehaviors
{
    public static readonly DependencyProperty BaseUrlProperty =
        DependencyProperty.RegisterAttached(
            "BaseUrl",
            typeof(string),
            typeof(EditorBindingBehaviors),
            new PropertyMetadata(string.Empty, OnBaseUrlChanged));

    public static string GetBaseUrl(DependencyObject o) =>
        (string)o.GetValue(BaseUrlProperty);

    public static void SetBaseUrl(DependencyObject o, string value) =>
        o.SetValue(BaseUrlProperty, value);

    private static void OnBaseUrlChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is WpfHtmlEditor editor && e.NewValue is string url)
            editor.BaseUrl = url;
    }
}

Then in XAML:

<wpfeditor:WpfHtmlEditor x:Name="MyEditor"
                         BodyHtml="{Binding HtmlContent}"
                         local:EditorBindingBehaviors.BaseUrl="{Binding ImageBaseUrl}" />

The same pattern works for DefaultFontFamily, DefaultFontSizeInPt, LicenseKey, or any other regular CLR property on the control.

Tracking every change for autosave / dirty state

BodyHtml with UpdateSourceTrigger=LostFocus writes back once per focus session, which is exactly what you want for typing performance but not enough for "autosave every 5 seconds" or "light up the Save button on the first edit". Use the editor's HtmlChanged event for those:

public event EventHandler<EventArgs> HtmlChanged;

It fires on every modification — keystroke, paste, undo, toolbar formatting command, drag & drop. The MVVM-clean way to forward it is a one-line code-behind handler that invokes an ICommand on the ViewModel:

<wpfeditor:WpfHtmlEditor x:Name="MyEditor"
                         BodyHtml="{Binding HtmlContent}"
                         HtmlChanged="MyEditor_HtmlChanged" />
private void MyEditor_HtmlChanged(object sender, EventArgs e)
{
    if (DataContext is MainViewModel vm && vm.MarkDirtyCommand.CanExecute(null))
        vm.MarkDirtyCommand.Execute(null);
}

The ViewModel now reacts to every edit without ever taking a reference to the control. A typical autosave implementation throttles the command (for example via DispatcherTimer reset on each invocation) and only writes to disk after the user pauses.

A complete worked example (CommunityToolkit.Mvvm)

MainViewModel.cs — a single ObservableObject with two bindable properties and a Save command:

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

public partial class MainViewModel : ObservableObject
{
    [ObservableProperty]
    private string htmlContent =
        "<p>Edit me, then click <b>Save</b>.</p>";

    [ObservableProperty]
    private bool isDirty;

    [RelayCommand(CanExecute = nameof(CanSave))]
    private void Save()
    {
        // Persist HtmlContent to your store...
        IsDirty = false;
    }

    private bool CanSave() => IsDirty;

    [RelayCommand]
    private void MarkDirty() => IsDirty = true;

    partial void OnIsDirtyChanged(bool value) => SaveCommand.NotifyCanExecuteChanged();
}

MainWindow.xaml:

<Window x:Class="DemoApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:wpfeditor="clr-namespace:SpiceLogic.HtmlEditor.WPF;assembly=SpiceLogic.HtmlEditor.WPF"
        Title="HTML Editor MVVM Demo" Height="600" Width="900">
    <DockPanel>
        <Button DockPanel.Dock="Top"
                Content="Save"
                Command="{Binding SaveCommand}" />

        <wpfeditor:WpfHtmlEditor x:Name="MyEditor"
                                 BodyHtml="{Binding HtmlContent,
                                                    Mode=TwoWay,
                                                    UpdateSourceTrigger=LostFocus}"
                                 HtmlChanged="MyEditor_HtmlChanged" />
    </DockPanel>
</Window>

MainWindow.xaml.cs:

using System;
using System.Windows;

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        DataContext = new MainViewModel();
    }

    private void MyEditor_HtmlChanged(object sender, EventArgs e)
    {
        if (DataContext is MainViewModel vm)
            vm.MarkDirtyCommand.Execute(null);
    }
}

Result: every edit lights up the Save button via IsDirty; the actual HTML is written back to the ViewModel only when the editor loses focus, so typing stays smooth. The ViewModel has zero references to WpfHtmlEditor, and the code-behind has exactly one line of glue.

Putting it all together: the rules

  1. Bind BodyHtml (or DocumentHtml when you need the full document) TwoWay with LostFocus. Never PropertyChanged.
  2. For everything that is not in the seven-DP list above, use a code-behind projection or a one-property attached behavior. The ViewModel stays free of WPF types.
  3. For autosave / dirty tracking, forward HtmlChanged to an ICommand on the ViewModel — do not lower the binding trigger.
  4. Set DefaultFontFamily / DefaultFontSizeInPt / DefaultForeColor once at construction (XAML attribute or constructor). They are not dependency properties; they are designed as startup-time defaults, not as live-bindable properties.
Last updated on May 14, 2026