Skip to content

Comments

MarkdownTextBlock: ThemeResource support, DP migration & performance fixes#785

Open
niels9001 wants to merge 6 commits intomainfrom
niels9001/mdtb-theming
Open

MarkdownTextBlock: ThemeResource support, DP migration & performance fixes#785
niels9001 wants to merge 6 commits intomainfrom
niels9001/mdtb-theming

Conversation

@niels9001
Copy link
Contributor

@niels9001 niels9001 commented Feb 24, 2026

MarkdownTextBlock: ThemeResource support, DP migration & performance fixes

Summary

This PR modernizes the MarkdownTextBlock control's theming architecture and fixes several performance/memory issues. The control now supports proper {ThemeResource} brushes that update automatically on Light/Dark/HighContrast theme switches — previously, theme changes had no effect on markdown styling.

Addresses: #611


What changed

Theming & DependencyProperty migration

Previously, all styling lived in plain C# classes (MarkdownThemes with auto-properties, wrapped by MarkdownConfig). This meant {ThemeResource} couldn't target them, and theme switches (Light ↔ Dark) had no effect on markdown brush colors.

New architecture: All 67+ styling properties are now DependencyProperties directly on MarkdownTextBlock. The intermediary MarkdownThemes and MarkdownConfig classes are deleted entirely. TextElement renderers (MyHeading, MyCodeBlock, MyQuote, MyTable, etc.) receive the MarkdownTextBlock control reference and read DPs directly from it at render time.

<controls:MarkdownTextBlock
    H1Foreground="{ThemeResource MyCustomBrush}"
    CodeBlockBackground="{StaticResource CodeBg}"
    InlineCodeCornerRadius="4" />

DPs added to MarkdownTextBlock.Properties.cs

Category Properties
Headings (18) H1H6 × FontSize, Foreground, FontWeight, Margin
Inline Code (7) InlineCodeBackground, InlineCodeForeground, InlineCodeBorderBrush, InlineCodeBorderThickness, InlineCodeCornerRadius, InlineCodePadding, InlineCodeFontSize, InlineCodeFontWeight
Code Blocks (9) CodeBlockBackground, CodeBlockForeground, CodeBlockBorderBrush, CodeBlockBorderThickness, CodeBlockPadding, CodeBlockMargin, CodeBlockFontFamily, CodeBlockCornerRadius
Quotes (8) QuoteBackground, QuoteForeground, QuoteBorderBrush, QuoteBorderThickness, QuoteMargin, QuotePadding, QuoteBarMargin, QuoteCornerRadius
Tables (6) TableHeadingBackground, TableBorderBrush, TableBorderThickness, TableCellPadding, TableMargin, TableCornerRadius
Horizontal Rule (4) HorizontalRuleBrush, HorizontalRuleThickness, HorizontalRuleMargin
Links (1) LinkForeground
Images (3) ImageMaxWidth, ImageMaxHeight, ImageStretch
Paragraphs/Lists (4) ParagraphMargin, ParagraphLineHeight, ListBulletSpacing, ListGutterWidth
Bold (1) BoldFontWeight
Config (3) BaseUrl, ImageProvider, SVGRenderer (previously on MarkdownConfig)

Default style (MarkdownTextBlock.xaml)

  • ThemeDictionaries with Default, Light, and HighContrast dictionaries define all brush resources (e.g., MarkdownTextBlockH1ForegroundTextFillColorPrimaryBrush). HighContrast uses SystemColorButtonTextColorBrush, SystemColorHotlightColorBrush, etc.
  • Non-brush values (font sizes, margins, thicknesses, corner radii, font weights) are set as literal values in style setters — not as named resources.
  • Follows the same pattern as SettingsControls in this repo.

Rendering architecture changes

  • WinUIRenderer — Removed Config property. Constructor now takes (MyFlowDocument, MarkdownTextBlock). Already had a MarkdownTextBlock property.
  • All 11 TextElement classes — Constructors changed from MarkdownThemes/MarkdownConfig parameter to MarkdownTextBlock. Each reads DPs directly (e.g., _control.H1FontSize, _control.CodeBlockBackground).
  • All 8 ObjectRenderers + HtmlWriter — Pass renderer.MarkdownTextBlock instead of renderer.Config.Themes.
  • Batched re-render — All theme DPs share a single OnThemePropertyChanged callback that uses DispatcherQueue.TryEnqueue with a boolean guard to coalesce multiple simultaneous DP changes (e.g., 20+ brush updates on theme switch) into one re-render.
  • ActualThemeChanged handler as belt-and-suspenders for theme switching, using the same batching guard.

Performance & memory fixes

  1. Fixed HttpClient socket leak (MyImage.cs)
    Each image load created new HttpClient(), never disposed. HttpClient maintains connection pools internally, so per-instance creation leaks socket handles. Replaced with a static readonly HttpClient shared across all image loads.

  2. Fixed Image.Loaded event handler leak (MyImage.cs)
    _image.Loaded += LoadImage was never unsubscribed. On every theme-change re-render, the old MyImage objects remained rooted via the event handler delegate, preventing garbage collection. Now unsubscribes immediately at the top of the handler.

  3. Fixed ActualThemeChanged event leak (MarkdownTextBlock.xaml.cs)
    The event was subscribed in the constructor but never unsubscribed. Moved to Loaded/Unloaded lifecycle handlers, matching the pattern used by the old CommunityToolkit MarkdownTextBlock control.

  4. Eliminated per-image DefaultSVGRenderer allocation (MyImage.cs)
    Every image without a custom SVG renderer created new DefaultSVGRenderer(). Since it's stateless, replaced with a static readonly singleton.

  5. Fixed double-scan on HtmlNodeCollection (HtmlWriter.cs)
    node.ChildNodes.Remove(node.ChildNodes.FirstOrDefault(...)) scanned the collection twice — once to find the node, once to remove it. Refactored to a single FirstOrDefault + null-check + Remove.

  6. Improved text buffer growth strategy (WinUIRenderer.cs)
    When text exceeded the 1024-char buffer, the entire buffer was replaced with an exact-fit ToCharArray() — likely too small for the next call, causing repeated reallocations. Now uses a doubling strategy (Math.Max(length, buffer.Length * 2)).

Sample updates

  • Example sample simplified — no longer needs Config binding; defaults come from the XAML style automatically.
  • Custom Theme sample updated to bind directly to control DPs via x:Bind function bindings.
  • "Apply Changes" button removed from theme options — changes are now live via bindings.

Breaking changes

  • MarkdownConfig and MarkdownThemes classes are deleted. Users who were setting Config should set properties directly on the control:
    - markdown.Config = new MarkdownConfig { Themes = new MarkdownThemes { H1FontSize = 32 } };
    + markdown.H1FontSize = 32;
  • BaseUrl, ImageProvider, SVGRenderer are now DPs on MarkdownTextBlock (previously on MarkdownConfig):
    - markdown.Config = new MarkdownConfig { ImageProvider = myProvider };
    + markdown.ImageProvider = myProvider;

Testing

  • XAML compiler: zero errors across all target frameworks
  • Existing ImageProviderConstraintTest updated to use new DP API
  • Theme switching verified: Light ↔ Dark ↔ HighContrast all update brushes correctly

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant