Getting and Setting HTML Value

Tomasz spent the morning arguing with a SQL Server table. His team is rebuilding the email-template authoring screen in their WPF customer-service desktop, the one a hundred call-centre agents use to send case responses. The previous tool stored each template as a complete HTML document -- DOCTYPE, head, the lot -- in an NVARCHAR(MAX) column. The new pipeline renders templates inside a larger email shell, so the <html> wrapper from the database is now a problem: two DOCTYPEs in the same outbound message, conflicting <head> blocks, mail clients reporting it as malformed.

The fix is to store only the inner markup -- what goes between the <body> tags. The WpfHtmlEditor exposes exactly that distinction through two dependency properties, and choosing between them is the first decision Tomasz makes on this screen.

Two properties, two storage models

BodyHtml returns the markup inside the <body> element and nothing else -- no DOCTYPE, no <html>, no <head>. That is the right shape for a fragment that will be embedded somewhere later: an email body composed inside a larger shell, a CMS rich-text field, a PDF report cell, a chat message. Tomasz's template-storage column wants this.

DocumentHtml returns the complete document, DOCTYPE through closing </html>, with the head block intact including any <link> stylesheet references and <style> rules the user added. That is the right shape for a standalone HTML file -- a newsletter the user will open directly in a browser, an exported report, a self-contained snapshot saved to disk.

Side-by-side comparison of BodyHtml output (inner markup only) versus DocumentHtml output (full DOCTYPE-prefixed document)

The XAML he actually wrote

Both properties are dependency properties registered with two-way binding on by default and an UpdateSourceTrigger of LostFocus. Tomasz binds BodyHtml to the template view-model and never touches the editor from code-behind:

<Window xmlns:editor="clr-namespace:SpiceLogic.HtmlEditor.WPF;assembly=SpiceLogic.HtmlEditor.WPF"
        DataContext="{Binding TemplateEditorViewModel, Source={StaticResource Locator}}">
    <editor:WpfHtmlEditor x:Name="TemplateEditor"
                          BodyHtml="{Binding TemplateBody, Mode=TwoWay,
                                              UpdateSourceTrigger=LostFocus}" />
</Window>

The LostFocus default is deliberate. Per-keystroke updates would generate hundreds of source updates while a user types a paragraph, each one re-serialising the body HTML. Updating when focus leaves the editor matches typical form semantics -- the agent finishes the field, tabs out, and the view-model has the latest value to validate or save.

The save handler

When the agent clicks Save, the view-model writes TemplateBody to the database column. The binding is already in sync because the Save button takes focus before its click handler runs. For the safety case where Save is triggered by a hotkey while the editor still has focus, Tomasz calls UpdateBindings() on the editor explicitly -- the method pushes BodyHtml, DocumentHtml, and DocumentTitle back to their binding sources synchronously:

private async Task SaveAsync()

{

    TemplateEditor.UpdateBindings();

    await _templateRepo.SaveAsync(_viewModel.TemplateId, _viewModel.TemplateBody);

}
WPF window with WpfHtmlEditor two-way bound to a ViewModel, the edited content flowing back to the template view-model property on lost focus

The export feature that came two sprints later

A request lands from the QA team: support agents want to download a finished template as a standalone HTML file to preview in Outlook before scheduling it. The body-only string in the database is not enough -- Outlook needs the full document. The editor has the answer in the other property:

private void ExportTemplate_Click(object sender, RoutedEventArgs e)

{

    var dialog = new SaveFileDialog { Filter = "HTML files (*.html)|*.html" };

    if (dialog.ShowDialog() == true)

        File.WriteAllText(dialog.FileName, TemplateEditor.DocumentHtml);

}

The same editor instance, no extra configuration -- one property for the fragment shape that goes into the database, another for the standalone shape that goes onto disk. The user types once and both forms are available.

The migration step Tomasz had to write once

Before any of this could ship, the existing rows in the templates table still held full documents. A one-time migration loaded each row into a hidden editor instance, assigned the old value to DocumentHtml, then read back BodyHtml and wrote that to the same row. The editor's own parser took care of stripping the outer wrapper -- exactly the kind of job a hand-rolled regex would have failed at on the first inline style block.

Last updated on May 15, 2026