Instruction file imported from tobitege/Flowery.Uno (
.cursor/rules/uno.mdc). Copyright stays with the author.
Flowery.Uno MANDATORY Development Notes
This file captures learnings and patterns discovered during Flowery.Uno development for future reference.
🚨 READ THIS FIRST (top crash-prevention rules)
If you only read the first ~50 lines of this file, read this section. These rules prevent the most common Uno “mystery” runtime crashes:
- ContentControls that rebuild
Content: ALWAYS detach before re-parenting.- Pattern:
_userContent = Content; Content = null; BuildVisualTree(); _presenter.Content = _userContent; - See “UIElement can only have one parent” (search for
### 19).
- Pattern:
- Never bind a
ContentPresenterback tothis.Contentin a control that later setsContent = _rootGrid(creates self-parenting / re-parenting issues).- See the anti-pattern example under
### 19.
- See the anti-pattern example under
- Don’t use
PathIcondynamically in code-behind (Uno can throwArgumentException: Value does not fall within the expected range.).- Use
Microsoft.UI.Xaml.Shapes.PathorFlowery.Helpers.FloweryPathHelpersinstead (search for### 18).
- Use
- Use
DaisyControlExtensions.Iconfor button icons - This is the GOLD STANDARD (see section below). - Ambiguous type names:
Path,Color, etc. often need aliasing/qualification.- Example:
using Path = Microsoft.UI.Xaml.Shapes.Path;(search for### 12).
- Example:
- Do NOT merge
ms-appx:///uno.toolkit.*/Styles/Generic.xaml.Uno.Toolkit.*packages do not shipStyles/Generic.xaml. Adding that dictionary will fail to load resources and can crash at startup. ShadowContainer only needs the package reference.
Neumorphic Takeaways
For the distilled, field-tested fixes and integration notes from recent stability work, see llms-static/neumorphic.md → Field-Tested Integration Notes (Session Takeaways).
ThemeShadow + Elevation Learnings
- ThemeShadow requires explicit receivers: add the receiver to
ThemeShadow.Receivers(see Uno tests in!uno/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Media/ThemeShadowTests). - Translation.Z defines shadow depth: the casting element must have a
TranslationZ (e.g.,0,0,6). - CornerRadius is respected when ThemeShadow is applied directly to
BorderorRectanglewith radius properties (no custom masking required). - WASM/Skia elevation: for rounded shadows, apply
SetElevationto the template’sBorder(e.g.,ButtonBorder), not theButtoncontrol itself.
Pitfalls (Session)
These are compile-time issues that should be avoided up front:
- Do not use
??with different operand types (e.g.,Border ?? DaisyCard). Cast to a shared base likeFrameworkElementfirst. - Do not assign
UIElementtoFrameworkElementwithout an explicit cast and a null/type check. - Do not reference non-existent WinUI/Uno members like
FrameworkElement.IsVisibleChanged; use supported events orRegisterPropertyChangedCallback. - Do not access internal or private helpers (e.g.,
PlatformCompatibility) from outside their assembly. - Do not set
nullinto non-nullable reference types; update the type or use a nullable value. - Do not use
x:Bindpaths that are not real properties on the page (e.g.,Localization); ensure the property exists and follow the required localization binding pattern.
WASM/Browser (Skia) Heads-Up (Session)
These are the practical fixes and gotchas encountered when getting the Browser head running with Skia:
- Use
Microsoft.NET.Sdk.WebAssemblyfor the Browser head when usingUno.WinUI.Runtime.Skia.WebAssembly.Browser. Do NOT referenceUno.WinUI.Runtime.WebAssembly(triggersUNOB0017). - Target
net9.0withRuntimeIdentifier=browser-wasmand run viadotnet runon the project (not the output folder) to avoidhostpolicy.dllself-contained errors. - Use
HostBuilder.UseWebAssembly()directly; avoid reflection-based host builder hacks (inaccessible method errors). - Fix culture crashes by disabling invariant globalization and including ICU data:
<InvariantGlobalization>false</InvariantGlobalization><WasmIncludeFullIcuData>true</WasmIncludeFullIcuData>
- Keep SkiaSharp versions aligned across managed + native:
SkiaSharpandSkiaSharp.NativeAssets.WebAssemblyMUST match (e.g.,3.119.1) or you will hit undefined symbol errors.
- Static web assets duplicates (library layout + root assets) cause:
Two assets found targeting the same path with incompatible asset kinds- Fix by disabling root asset copies for
net9.0library projects and usingms-appx:///Flowery.Uno.Gallery/Assets/...paths. - Add a Browser-head build target that
RemoveDuplicateson@(UnoAllCopyToOutputItems)before_UnoAssetsGetCopyToPublishDirectoryItems.
- CSP warnings (workers,
unsafe-eval) andWEBGL_invalid_enummessages are expected in debug and are not fatal. - Keep Browser script ports aligned (5236) to avoid mismatched logs vs URL.
ms-appx:///is the correct scheme for Content assets in Uno (including WASM); it is supported byImage/BitmapImage.- Referencing assets in code can use
ms-appx:///Assets/...orms-appx:///AssemblyName/Assets/...and can be bound as a string. - Uno 4.7+ requires exact-case assembly names in
ms-appx:///AssemblyName/...(case-sensitive). - Library assets require
<GenerateLibraryLayout>true</GenerateLibraryLayout>in the library.csproj. - Library assets are referenced via
ms-appx:///[LibraryName]/[AssetPath]and viaStorageFile.GetFileFromApplicationUriAsync. - For non-WinAppSDK targets, library asset names should be lowercased in the
ms-appxURI. StorageFileHelper.ExistsInPackage("Assets/...")can be used to confirm asset presence at runtime.
Testing (Runtime Tests)
Problems, causes, and effective fixes
-
Windows runner COMExceptions / missing WinUI theme resources
- Symptom:
Cannot locate resource from 'ms-appx:///Microsoft.UI.Xaml/Themes/themeresources.xaml'and control construction COMExceptions. - Cause: Build command disabled PRI/resource generation and copy (
AppxGeneratePriEnabled=false,IncludeCopyLocalFilesOutputGroup=false). - Fix: Keep
EnableCoreMrtTooling=false, but remove those two properties in the Windows runner build args so WinUI resources load.
- Symptom:
-
Flowery theme resources missing in runtime tests
- Symptom:
ms-appx:///Flowery.Uno/Themes/Generic.xamlnot found; Daisy brushes missing. - Cause: Runtime test output only had
Themes/at app root, notFlowery.Uno/Themes/(library layout path). - Fix: In
Flowery.Uno.Gallery.Windows/App.xaml.cs, whenisRuntimeTests:- Copy
Themes→Flowery.Uno/ThemesunderAppContext.BaseDirectory. - Load
ms-appx:///Flowery.Uno/Themes/Generic.xaml. - Still add
XamlControlsResources(log if it fails). - Call
EnsureGalleryResources(isRuntimeTests)before the runtime-test early return so resources exist during tests.
- Copy
- Symptom:
-
Runtime tests not picking up
--runtime-testsargs- Symptom: Test app runs but no results produced / hangs.
- Cause: Args not reliably passed when running the Windows app via test host.
- Fix: Add env var fallback:
FLOWERY_RUNTIME_TESTS_PATH.RuntimeTestArgumentschecks env var first.- Both Windows/Skia runners set
FLOWERY_RUNTIME_TESTS_PATH.
-
Windows runner launching the wrong host
- Symptom: Running
dotnet <dll>bypassed Windows app packaging behavior. - Fix: Prefer the
.exe(Path.ChangeExtension(TargetPath, ".exe")) when it exists.
- Symptom: Running
Correct setup snapshot
Flowery.Uno.RuntimeTeststargets:net9.0;net9.0-windows10.0.19041.- Windows runner build args:
-p:EnableCoreMrtTooling=false(do not disable PRI or copy-local outputs). - Windows runner execution: run the app
.exewhen available; always setFLOWERY_RUNTIME_TESTS_PATH. App.xaml.csfor Windows: load runtime-test resources before the early return; copyThemesintoFlowery.Uno/Themesfor ms-appx resolution.
✅ CI / GitHub Actions (Uno + gh CLI)
Uno templates (CI generation)
Uno project templates can generate CI pipelines:
dotnet new unoapp -ci github
dotnet new unoapp -ci azure
dotnet new unoapp -ci none
GitHub CLI (workflow operations)
# List workflows
gh workflow list
# Trigger a workflow (requires on: workflow_dispatch)
gh workflow run .github/workflows/ci.yml
# Watch a run
gh run watch <run-id>
# View run details / logs
gh run view <run-id> --log-failed
⭐ GOLD STANDARD: Button Icons via Attached Property
This is the official Uno.Themes pattern for adding icons to buttons. It avoids all UIElement parentage issues.
Why This Pattern?
- UIElements can only have ONE parent. Passing
PathIconasContentoften causes silent failures. - Attached properties allow the button to build its own icon presenter, avoiding parentage issues.
- Supports icon + text on the same button (not mutually exclusive).
- Foreground inheritance is handled correctly for different button variants/states.
Using DaisyControlExtensions.Icon (Recommended)
<!-- Icon-only button -->
<daisy:DaisyButton Shape="Circle" Variant="Primary">
<daisy:DaisyControlExtensions.Icon>
<PathIcon Width="16" Height="16" Data="M12 4.5v15m7.5-7.5h-15" />
</daisy:DaisyControlExtensions.Icon>
</daisy:DaisyButton>
<!-- Icon + text button -->
<daisy:DaisyButton Content="Save" Variant="Primary">
<daisy:DaisyControlExtensions.Icon>
<PathIcon Width="16" Height="16" Data="M..." />
</daisy:DaisyControlExtensions.Icon>
</daisy:DaisyButton>
<!-- Icon on the right side -->
<daisy:DaisyButton Content="Next">
<daisy:DaisyControlExtensions.Icon>
<PathIcon Data="M..." />
</daisy:DaisyControlExtensions.Icon>
<daisy:DaisyControlExtensions.IconPlacement>Right</daisy:DaisyControlExtensions.IconPlacement>
</daisy:DaisyButton>
Using TriggerIconData for DaisyFab (Best Practice)
For the FAB trigger button, use TriggerIconData (a string containing the path data). This is the safest approach because we create the PathIcon internally, completely avoiding UIElement parentage issues.
<daisy:DaisyFab TriggerVariant="Secondary" Size="Medium"
TriggerIconData="M12 4.5v15m7.5-7.5h-15"
TriggerIconSize="16">
<StackPanel>
<daisy:DaisyButton Shape="Circle">
<PathIcon Width="16" Height="16" Data="M3 9.5l9-7 9 7V20..." />
</daisy:DaisyButton>
</StackPanel>
</daisy:DaisyFab>
Available Attached Properties
| Property | Type | Default | Description |
|---|---|---|---|
DaisyControlExtensions.Icon |
IconElement |
null |
The icon to display |
DaisyControlExtensions.IconWidth |
double |
NaN |
Explicit icon width (auto if NaN) |
DaisyControlExtensions.IconHeight |
double |
NaN |
Explicit icon height (auto if NaN) |
DaisyControlExtensions.IconPlacement |
IconPlacement |
Left |
Position: Left, Right, Top, Bottom |
DaisyControlExtensions.IconSpacing |
double |
8.0 |
Spacing between icon and content |
DaisyControlExtensions.AlternateContent |
object |
null |
For toggle controls: content when checked |
Note: These properties use
[Bindable]and[DynamicDependency]attributes for proper XAML binding and AOT compilation safety, following the Uno.Themes pattern.
❌ DON'T: Set PathIcon as Direct Content
<!-- ❌ FAILS SILENTLY: PathIcon becomes child of DaisyButton,
then can't be re-parented to internal presenter -->
<daisy:DaisyButton Shape="Circle" Variant="Primary">
<PathIcon Data="M..." />
</daisy:DaisyButton>
✅ DO: Use the Attached Property Pattern
<!-- ✅ WORKS: Icon is picked up by button and placed in its own presenter -->
<daisy:DaisyButton Shape="Circle" Variant="Primary">
<daisy:DaisyControlExtensions.Icon>
<PathIcon Data="M..." />
</daisy:DaisyControlExtensions.Icon>
</daisy:DaisyButton>
Implementation Reference
The pattern is implemented in:
Flowery.Controls.DaisyControlExtensions- Attached propertiesFlowery.Controls.DaisyButton.BuildIconLayoutIfNeeded()- Builds icon+content panelFlowery.Controls.DaisyFab.TriggerIcon- FAB trigger icon property
📚 Uno Platform Setup
Control Library Requirements
-
Themes/Generic.xamlis REQUIRED - Even if empty, control libraries MUST have this file:<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <!-- Control themes go here --> </ResourceDictionary> -
UNOB0008 Workaround - WinUI class libraries with XAML can't use
dotnet buildby default:<!-- Add to .csproj --> <UnoDisableValidateWinAppSDK3548>true</UnoDisableValidateWinAppSDK3548> -
GenerateLibraryLayoutmust betruefor control libraries:<GenerateLibraryLayout>true</GenerateLibraryLayout> -
SDK auto-includes XAML files - Don't manually add
<Page Include="..."/>(causes duplicate errors) -
App.xaml library reference (if needed):
<ResourceDictionary Source="ms-appx:///Flowery.Uno/Themes/Generic.xaml" />
🛠️ Critical Patterns & Solutions
Code-Only DataTemplates (Runtime XAML Parsing)
Problem: You need a complex DataTemplate (e.g. for a ComboBox or ListView) in a code-only control, but don't want to bundle a separate .xaml file or struggle with programmatic FrameworkElementFactory (which is deprecated/limited).
Solution: Use XamlReader.Load to parse a XAML string at runtime. This allows full usage of bindings and converters without needing a physical XAML file.
Requirements:
- Namespace:
using Microsoft.UI.Xaml.Markup; - Define the template locally in C#
- Assign in
OnLoaded(not constructor) - Ensure
Itemsare populated before assigning
Example (DaisyThemeDropdown):
private void OnLoaded(object sender, RoutedEventArgs e)
{
// 1. Create DataTemplate string with standard namespaces
string xaml = @"
<DataTemplate xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'>
<StackPanel Orientation='Horizontal' Spacing='4'>
<Ellipse Width='10' Height='10' Fill='{Binding Primary}'/>
<TextBlock Text='{Binding DisplayName}' VerticalAlignment='Center'/>
</StackPanel>
</DataTemplate>";
try
{
// 2. Parse and assign
ItemTemplate = (DataTemplate)XamlReader.Load(xaml);
}
catch (Exception ex)
{
// 3. Always provide a fallback!
System.Diagnostics.Debug.WriteLine($"Template Error: {ex.Message}");
DisplayMemberPath = "DisplayName";
}
}
Why this matters:
- Performance: Efficient enough for UI controls
- Stability: Avoids
PrepareContainerForItemOverridebugs withComboBoxselection display - Flexibility: Allows using
Bindingsyntax which is hard to replicate in pure code - Selection Support: The template automatically applies to both the list AND the selection box (unlike
DelayLoadedapproaches)
Code-Behind Visual Wrappers (Dashed Borders)
Problem: WinUI Border control DOES NOT support StrokeDashArray. You cannot achieve dashed borders on standard buttons or containers using standard properties.
Solution: Wrap the content in a Grid and overlay a Microsoft.UI.Xaml.Shapes.Rectangle, which does support StrokeDashArray.
Pattern:
- Check style in
OnLoaded. - If dashed style:
- Create a
Gridwrapper. - Move
Contentinto wrapper. - Add
RectanglewithStrokeDashArrayto wrapper. - Set control
Contentto wrapper. - Set native
BorderThicknessto 0.
- Create a
private void BuildDashedBorderIfNeeded() {
if (Style != Dash) return;
// 1. Capture content and standard padding
var original = Content;
var padding = Padding;
Padding = new Thickness(0); // Removing padding from button so wrapper fills it
// 2. Create wrapper
var grid = new Grid();
// 3. Add ContentPresenter with Margin to simulate padding
var innerContent = new ContentPresenter {
Content = original,
Margin = padding // Applied here instead!
};
grid.Children.Add(innerContent);
// 4. Add Dashed Border (fills the grid)
grid.Children.Add(new Rectangle {
StrokeDashArray = new DoubleCollection { 4, 2 },
// ...
});
Content = grid;
// 5. Ensure wrapper fills the button
VerticalContentAlignment = VerticalAlignment.Stretch;
HorizontalContentAlignment = HorizontalAlignment.Stretch;
}
Critical Detail: The Button's Padding must be moved to the inner content's Margin. If you leave Padding on the Button, the dashed border will be drawn inside the padding, making the button look too small.
Full-Screen Overlay Alignment
Problem: Custom controls designed to be overlays (like Modals) appear in the top-left corner or don't cover the screen, even if the inner container is centered.
Solution: Explicitly set HorizontalAlignment and VerticalAlignment to Stretch on both the control itself (in constructor) AND the root visual element (Grid/Border).
Code:
public DaisyModal() {
// 1. Stretch the control itself
HorizontalAlignment = HorizontalAlignment.Stretch;
VerticalAlignment = VerticalAlignment.Stretch;
_root = new Grid {
// 2. Stretch the root element
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch
};
// 3. Center the inner dialogue
_dialogBorder.HorizontalAlignment = HorizontalAlignment.Center;
_dialogBorder.VerticalAlignment = VerticalAlignment.Center;
}
Manual Visual State Management
Problem: VisualStateManager in XAML can be verbose or limited for code-only custom controls that combine multiple independent state axes (e.g., Theme + Variant + MouseState).
Solution: Hook into pointer events (PointerEntered, PointerPressed, etc.) and call a unified ApplyVisualState() method.
Pattern:
-
Define unified state applicator:
ApplyCurrentPointerState(). -
Subscribe to events:
Loaded += (s,e) => { PointerEntered += (s,e) => { _isHover = true; ApplyCurrentPointerState(); }; PointerExited += (s,e) => { _isHover = false; ApplyCurrentPointerState(); }; }; -
In
ApplyCurrentPointerState:- Resolve BaseBrush based on Theme/Variant.
- Modify BaseBrush based on
_isHover/_isPressed(e.g.WithOpacity). - Apply to
Background/BorderBrush.
🎨 Animation Patterns (Composition API)
Uno Platform supports hardware-accelerated animations via the Composition API. This is the preferred method over Storyboard-based animations for smooth performance.
Basic Setup
using System.Numerics;
using Microsoft.UI.Composition;
using Microsoft.UI.Xaml.Hosting;
// Get visual and compositor from a UI element
var visual = ElementCompositionPreview.GetElementVisual(element);
var compositor = visual.Compositor;
// Set center point for rotation/scale (required!)
visual.CenterPoint = new Vector3((float)(size / 2), (float)(size / 2), 0);
Rotation Animation (Spinner)
IMPORTANT: Uno Platform's Visual only supports animating "RotationAngle" (in radians), NOT "RotationAngleInDegrees". Use the helper methods in PlatformCompatibility for degree-to-radian conversion:
// Option 1: Use the helper to create and start the animation (recommended)
PlatformCompatibility.StartRotationAnimationInDegrees(
visual,
[(0f, 0f), (1f, 360f)], // (progress, angleDegrees)
TimeSpan.FromMilliseconds(750));
// Option 2: Manual creation with conversion helper
var animation = compositor.CreateScalarKeyFrameAnimation();
animation.InsertKeyFrame(0f, 0f);
animation.InsertKeyFrame(1f, PlatformCompatibility.DegreesToRadians(360f)); // Convert to radians!
animation.Duration = TimeSpan.FromMilliseconds(750);
animation.IterationBehavior = AnimationIterationBehavior.Forever;
visual.StartAnimation("RotationAngle", animation); // Use "RotationAngle", NOT "RotationAngleInDegrees"!
Scale Animation (Pulse/Bounce)
var scaleAnimation = compositor.CreateVector3KeyFrameAnimation();
scaleAnimation.InsertKeyFrame(0f, new Vector3(1f, 1f, 1f));
scaleAnimation.InsertKeyFrame(0.5f, new Vector3(1.3f, 1.3f, 1f));
scaleAnimation.InsertKeyFrame(1f, new Vector3(1f, 1f, 1f));
scaleAnimation.Duration = TimeSpan.FromMilliseconds(1000);
scaleAnimation.IterationBehavior = AnimationIterationBehavior.Forever;
visual.StartAnimation("Scale", scaleAnimation);
Opacity Animation (Fade/Blink)
var opacityAnimation = compositor.CreateScalarKeyFrameAnimation();
opacityAnimation.InsertKeyFrame(0f, 1f);
opacityAnimation.InsertKeyFrame(0.5f, 0.4f);
opacityAnimation.InsertKeyFrame(1f, 1f);
opacityAnimation.Duration = TimeSpan.FromMilliseconds(1500);
opacityAnimation.IterationBehavior = AnimationIterationBehavior.Forever;
visual.StartAnimation("Opacity", opacityAnimation);
Delayed Animation (Staggered Dots)
IMPORTANT: animation.DelayTime does NOT work on Uno Skia/WASM backends. Use PlatformCompatibility.StartAnimation which handles delays correctly across platforms:
// ❌ DON'T use DelayTime directly (doesn't work on Skia/WASM)
scaleAnimation.DelayTime = TimeSpan.FromMilliseconds(index * 150);
// ✅ DO use PlatformCompatibility.StartAnimation
PlatformCompatibility.StartAnimation(visual, "Scale", scaleAnimation, TimeSpan.FromMilliseconds(index * 150));
Stopping Animations
visual.StopAnimation("RotationAngle"); // Use "RotationAngle", NOT "RotationAngleInDegrees"!
visual.StopAnimation("Scale");
visual.StopAnimation("Opacity");
Animation Lifecycle
- Start animations in element's
Loadedevent (after it's in visual tree) - Stop animations in
Unloadedevent to prevent memory leaks - Track
_isAnimatingflag to prevent starting animations after unload
🎯 Control Development Patterns
Programmatic Visual Tree (No XAML Templates)
For controls that need runtime flexibility, build the visual tree in code:
public class MyControl : ContentControl
{
private Grid? _rootGrid;
public MyControl()
{
DefaultStyleKey = typeof(MyControl);
Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
BuildVisualTree();
}
private void BuildVisualTree()
{
_rootGrid = new Grid();
// ... build tree
Content = _rootGrid;
}
}
Dependency Properties
public static readonly DependencyProperty VariantProperty =
DependencyProperty.Register(
nameof(Variant),
typeof(MyVariantEnum),
typeof(MyControl),
new PropertyMetadata(MyVariantEnum.Default, OnAppearanceChanged));
public MyVariantEnum Variant
{
get => (MyVariantEnum)GetValue(VariantProperty);
set => SetValue(VariantProperty, value);
}
private static void OnAppearanceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is MyControl control)
{
control.RebuildVisual();
}
}
Tab Selection Change Callback
// Use RegisterPropertyChangedCallback for DependencyProperty changes
MainTabs.RegisterPropertyChangedCallback(DaisyTabs.SelectedIndexProperty, OnTabSelectionChanged);
private void OnTabSelectionChanged(DependencyObject sender, DependencyProperty dp)
{
var selectedIndex = MainTabs.SelectedIndex;
// Handle tab change
}
🎨 XAML Styles (WinUI/Uno Specifics)
This section covers WinUI/Uno-specific style patterns based on official Microsoft guidance.
Implicit vs Explicit Styles
Styles without x:Key apply automatically to all controls of the target type (implicit). Styles with x:Key must be explicitly referenced.
<Page.Resources>
<!-- Implicit: applies to ALL Buttons in this page -->
<Style TargetType="Button">
<Setter Property="Background" Value="Blue" />
</Style>
<!-- Explicit: must be referenced via Style="{StaticResource MyButtonStyle}" -->
<Style x:Key="MyButtonStyle" TargetType="Button">
<Setter Property="Background" Value="Red" />
</Style>
</Page.Resources>
<Button Content="Blue (implicit)" />
<Button Content="Red (explicit)" Style="{StaticResource MyButtonStyle}" />
BasedOn for Style Inheritance
Use BasedOn to inherit from another style. The derived style must target the same type or a subclass.
<Page.Resources>
<Style x:Key="BaseButtonStyle" TargetType="Button">
<Setter Property="Height" Value="40" />
<Setter Property="Padding" Value="16,8" />
</Style>
<!-- Inherits Height and Padding, adds Background -->
<Style x:Key="PrimaryButtonStyle" TargetType="Button" BasedOn="{StaticResource BaseButtonStyle}">
<Setter Property="Background" Value="{ThemeResource DaisyPrimaryBrush}" />
<Setter Property="Foreground" Value="{ThemeResource DaisyPrimaryContentBrush}" />
</Style>
</Page.Resources>
For system controls using WinUI styles, use the Default<ControlName>Style pattern:
<!-- Base on system TextBox style, then customize -->
<Style TargetType="TextBox" BasedOn="{StaticResource DefaultTextBoxStyle}">
<Setter Property="Foreground" Value="Blue"/>
</Style>
Lightweight Styling (Per-Control Resource Overrides)
Override theme resources for a single control by placing resources in the control's local Resources:
<Button Content="Custom Colors">
<Button.Resources>
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="ButtonBackground" Color="Transparent"/>
<SolidColorBrush x:Key="ButtonForeground" Color="MediumSlateBlue"/>
<SolidColorBrush x:Key="ButtonBorderBrush" Color="MediumSlateBlue"/>
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="ButtonBackground" Color="Transparent"/>
<SolidColorBrush x:Key="ButtonForeground" Color="Coral"/>
<SolidColorBrush x:Key="ButtonBorderBrush" Color="Coral"/>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</Button.Resources>
</Button>
State-specific resources use suffixes: ButtonBackgroundPointerOver, ButtonForegroundPressed, ButtonBorderBrushDisabled, etc.
Custom Control Styling with Resource Aliasing
For custom controls that should follow system styling patterns, alias your custom resources to system resources:
<!-- In your control's style -->
<Style TargetType="local:MyCustomButton">
<Setter Property="Background" Value="{ThemeResource MyCustomButtonBackground}" />
<Setter Property="BorderBrush" Value="{ThemeResource MyCustomButtonBorderBrush}"/>
</Style>
<!-- In your ResourceDictionary, alias to system resources -->
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Default">
<StaticResource x:Key="MyCustomButtonBackground" ResourceKey="ButtonBackground" />
<StaticResource x:Key="MyCustomButtonBorderBrush" ResourceKey="ButtonBorderBrush" />
</ResourceDictionary>
<ResourceDictionary x:Key="Light">
<StaticResource x:Key="MyCustomButtonBackground" ResourceKey="ButtonBackground" />
<StaticResource x:Key="MyCustomButtonBorderBrush" ResourceKey="ButtonBorderBrush" />
</ResourceDictionary>
<ResourceDictionary x:Key="HighContrast">
<StaticResource x:Key="MyCustomButtonBackground" ResourceKey="ButtonBackground" />
<StaticResource x:Key="MyCustomButtonBorderBrush" ResourceKey="ButtonBorderBrush" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
Note: You must define all three theme dictionaries (
Default,Light,HighContrast) for proper theme switching.
Derived Control Styles
When deriving a custom control from a WinUI control, it won't inherit WinUI styles by default. Apply them explicitly:
<ContentDialog x:Class="MyApp.CustomDialog" ...>
<ContentDialog.Resources>
<!-- Apply WinUI's default ContentDialog style to our derived type -->
<Style TargetType="local:CustomDialog" BasedOn="{StaticResource DefaultContentDialogStyle}"/>
</ContentDialog.Resources>
</ContentDialog>
Style Scope and Precedence
| Location | Scope | Precedence (highest first) |
|---|---|---|
| Control.Resources | Single control | 1 (highest) |
| Page.Resources | Single page | 2 |
| App.xaml Resources | Entire app | 3 |
| Generic.xaml | Control library defaults | 4 (lowest) |
If the same key exists in multiple locations, the closest scope wins.
🎨 Theme Management
Resource Dictionary Approach
Themes are implemented as ResourceDictionary objects with color brushes:
public static ResourceDictionary Create(string themeName)
{
var palette = Palettes[themeName];
var resources = new ResourceDictionary();
// Colors
resources["DaisyPrimaryColor"] = ColorFromHex(palette.Primary);
// Brushes (for binding)
resources["DaisyPrimaryBrush"] = new SolidColorBrush(ColorFromHex(palette.Primary));
return resources;
}
Applying Themes at Runtime
// Merge theme resources into Application.Current.Resources
Application.Current.Resources.MergedDictionaries.Clear();
Application.Current.Resources.MergedDictionaries.Add(themeResources);
// Update RequestedTheme for light/dark
RootGrid.RequestedTheme = isDark ? ElementTheme.Dark : ElementTheme.Light;
Accessing Theme Resources in Code
if (Application.Current.Resources.TryGetValue("DaisyPrimaryBrush", out var brush) && brush is Brush b)
{
element.Fill = b;
}
else
{
// Fallback
element.Fill = new SolidColorBrush(Colors.White);
}
⚠️ Common Pitfalls
1. Colors Namespace
Colors is in Microsoft.UI namespace, not Windows.UI:
using Microsoft.UI; // For Colors.White, Colors.Black, etc.
using Windows.UI; // For Color struct only
2. Arc/Path for Spinner
WinUI doesn't have an Arc control like Avalonia. Use Path with ArcSegment:
var pathGeometry = new PathGeometry();
var pathFigure = new PathFigure { StartPoint = new Point(startX, startY) };
pathFigure.Segments.Add(new ArcSegment
{
Point = new Point(endX, endY),
Size = new Size(radius, radius),
SweepDirection = SweepDirection.Clockwise,
IsLargeArc = true
});
pathGeometry.Figures.Add(pathFigure);
path.Data = pathGeometry;
3. Visual Tree Timing
Elements must be in the visual tree before getting their Visual:
element.Loaded += (s, e) =>
{
var visual = ElementCompositionPreview.GetElementVisual(element);
// Now safe to animate
};
4. CenterPoint for Transforms
Always set CenterPoint before rotation/scale animations:
visual.CenterPoint = new Vector3((float)(width / 2), (float)(height / 2), 0);
5. TextElement.* attached properties are not supported in Uno XAML
Uno/WinUI XAML does not support Avalonia-style attached properties like:
TextElement.FontSize="{TemplateBinding BodyFontSize}"
This fails with:
WMC0010: Unknown attachable member 'TextElement.FontSize' on element 'ContentPresenter'
Fix pattern (used in this repo):
- Remove
TextElement.FontSizefrom XAML templates (e.g.Themes/DaisyControls.xaml) - Drive the inherited size from code, e.g. in
DaisyCard.ApplyAll():FontSize = BodyFontSize;
6. No overridable OnPropertyChanged(DependencyPropertyChangedEventArgs) in WinUI/Uno controls
WinUI/Uno controls do not have an overridable method with the signature:
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
Attempting this fails with:
CS0115: ... no suitable method found to override
Fix pattern (used in this repo):
- Use DP metadata callbacks when registering the property:
new PropertyMetadata(defaultValue, OnChanged)
- Or use
RegisterPropertyChangedCallback(...)when you need to observe a DP without replacing metadata.
7. x:Type is NOT supported in WinUI/Uno XAML
The x:Type markup extension does not exist in WinUI/Uno. This fails:
<!-- ❌ FAILS: x:Type doesn't exist -->
<Style TargetType="controls:DaisyTextArea" BasedOn="{StaticResource {x:Type controls:DaisyInput}}">
Error: WMC0001: Unknown type 'Type' in XML namespace 'http://schemas.microsoft.com/winfx/2006/xaml'
Fix pattern: Don't use BasedOn with type references. If inheritance is needed, handle it in C# code by extending the base class.
<!-- ✅ WORKS: Just target the derived type directly -->
<Style TargetType="controls:DaisyTextArea">
<Setter Property="MinHeight" Value="80" />
</Style>
8. ItemsControl has no Content property
ItemsControl does not have a Content property like ContentControl does. If you need both items collection AND a content property, you must choose one approach:
- Use
ContentControland manage children manually via a collection property - Use
ItemsControland access items via theItemscollection (but noContent)
// ❌ FAILS: ItemsControl has no Content property
public class MyGroup : ItemsControl
{
private void Build() { Content = _panel; } // CS0103: 'Content' does not exist
}
// ✅ WORKS: Use ContentControl and collect children from Content
public class MyGroup : ContentControl
{
private void CollectItems()
{
if (Content is Panel panel)
{
foreach (var child in panel.Children) { /* ... */ }
}
}
}
9. Don't hide inherited UIElement methods
UIElement has several methods that are easy to accidentally hide with custom methods:
UpdateLayout()- Forces a layout passMeasure()/Arrange()- Layout methodsFocus()- Focus management
Using the same name causes warning CS0108:
// ❌ WARNING CS0108: Hides inherited member
private void UpdateLayout() { /* ... */ }
// ✅ WORKS: Use a different name
private void RebuildLayout() { /* ... */ }
private void ApplyLayout() { /* ... */ }
10. Reference comparison warnings with DependencyProperty values
When comparing Content or other DP values to stored references, use ReferenceEquals() to avoid CS0252 warnings:
// ❌ WARNING CS0252: Unintended reference comparison
if (Content != null && Content != _rootGrid)
// ✅ WORKS: Explicit reference comparison
if (Content != null && !ReferenceEquals(Content, _rootGrid))
11. Duplicate enum/class definitions across files (CS0101)
When porting controls from Avalonia, check if enums/classes are already defined in other files before adding them to new control files. Common duplicates:
DaisyButtonVariant,DaisyButtonStyle→ already inDaisyButton.csDaisyCollapseVariant→ already inDaisyCollapse.cs- Event args classes like
ButtonGroupItemSelectedEventArgs
Before adding ANY enum or helper class:
// ❌ DON'T blindly copy from Avalonia source
public enum DaisyButtonVariant { ... } // May already exist!
// ✅ DO search existing files first:
// grep -r "enum DaisyButtonVariant" Controls/
// If found, just use it - don't redefine!
12. Ambiguous type references (CS0104)
When using types like Path, Color, or Orientation that exist in multiple namespaces, qualify them explicitly:
// ❌ FAILS: Ambiguous between Microsoft.UI.Xaml.Shapes.Path and System.IO.Path
private Path? _iconPath;
// ✅ WORKS: Use fully qualified type name
private Microsoft.UI.Xaml.Shapes.Path? _iconPath;
Common ambiguous types:
Path→Microsoft.UI.Xaml.Shapes.PathvsSystem.IO.PathColor→Windows.UI.ColorvsSystem.Drawing.ColorOrientation→Microsoft.UI.Xaml.Controls.Orientationvs others
13. Missing enum values when porting
When porting a control that references an enum from another control, ensure all required values exist:
// DaisyCollapse.cs has:
public enum DaisyCollapseVariant { Arrow, Plus }
// DaisyAccordion.cs needs 'None' for its indicator visibility
// ❌ FAILS: DaisyCollapseVariant.None doesn't exist
// ✅ FIX: Add the missing value to the original enum
public enum DaisyCollapseVariant { Arrow, Plus, None }
14. Border does not have a Cursor property in WinUI/Uno
Unlike WPF, WinUI/Uno Border does not have a Cursor property. Cursor styling must be done differently or omitted.
// ❌ FAILS: Border doesn't have Cursor property
_headerBorder = new Border
{
Cursor = new Microsoft.UI.Input.InputCursor(...) // CS0117
};
// ✅ WORKS: Remove cursor styling for Uno controls
_headerBorder = new Border
{
CornerRadius = new CornerRadius(8),
Padding = new Thickness(16, 12, 16, 12)
};
15. Border is not a Control - pattern matching fails
Border inherits from FrameworkElement, not Control, so pattern matching Control to Border will fail.
// ❌ FAILS: Control can't be matched to Border (CS8121)
if (element is Control control)
{
if (control is Border border) { ... } // Border is not a Control!
}
// ✅ WORKS: Check for specific types separately
if (element is Button button)
{
button.CornerRadius = ...;
}
// For Border, check UIElement directly if needed
16. FlowerySizeManager.IgnoreGlobalSize is a method, not a property
To check if a control should ignore global size, use the GetIgnoreGlobalSize method:
// ❌ FAILS: IgnoreGlobalSize is not a property (CS0117)
var effectiveSize = FlowerySizeManager.IgnoreGlobalSize.GetValue(this)
? Size
: FlowerySizeManager.CurrentSize;
// ✅ WORKS: Use the method directly
var effectiveSize = FlowerySizeManager.GetIgnoreGlobalSize(this)
? Size
: FlowerySizeManager.CurrentSize;
17. Avoid manual Width calculation for stretched content (Sidebar Clipping)
When calculating container widths manually to simulate Stretch behavior, specific issues arise when a ScrollViewer is involved.
Problem:
If you manually set Width = RootContainer.ActualWidth - Margins, you fail to account for the vertical ScrollBar width potentially appearing in the ScrollViewer. This results in content being wider than the viewport, causing clipping (e.g., right side cut off).
Solution:
Avoid setting Width manually. Rely on HorizontalAlignment="Stretch" and let the layout system handle the viewport constraints.
// ❌ FAILS: Content clipped when scrollbar appears
MyPanel.Width = RootBorder.ActualWidth - Margin.Left - Margin.Right;
// ✅ WORKS: Let layout system handle it
MyPanel.HorizontalAlignment = HorizontalAlignment.Stretch;
// MyPanel.Width is not set
18. PathIcon cannot be used dynamically - use FloweryPathHelpers instead
In Uno Platform, using PathIcon in code-behind causes ArgumentException: Value does not fall within the expected range. Use the FloweryPathHelpers static class which creates Path elements with PathGeometry.
using Flowery.Services;
// ❌ FAILS: PathIcon throws ArgumentException at runtime
_previousButton.Content = new PathIcon {
Data = (Geometry?)Application.Current.Resources["DaisyIconChevronLeft"],
Width = 16,
Height = 16
};
// ✅ WORKS: Use FloweryPathHelpers
_previousButton.Content = FloweryPathHelpers.CreateChevron(isLeft: true);
_nextButton.Content = FloweryPathHelpers.CreateChevron(isLeft: false);
⚠️ Critical Warning:
Do NOT try to look up Geometry resources via Application.Current.Resources and assign them to Path.Data manually. This retrieves a shared instance, which will cause crash-on-startup loops (ArgumentException) if assigned to multiple elements. Always use FloweryPathHelpers which creates fresh geometry instances.
Available helper methods in FloweryPathHelpers:
| Method | Description |
|---|---|
CreateChevron(bool isLeft, double size = 12, double strokeThickness = 2, Brush? stroke = null) |
Left/right arrow |
CreatePlus(double size = 12, double strokeThickness = 2, Brush? stroke = null) |
Plus (+) icon |
CreateMinus(double size = 12, double strokeThickness = 2, Brush? stroke = null) |
Minus (-) icon |
CreateClose(double size = 12, double strokeThickness = 2, Brush? stroke = null) |
Close (X) icon |
Alternative: Use SymbolIcon for common Windows icons
For simple, built-in icons (like user silhouettes for avatars), SymbolIcon is simpler than FloweryPathHelpers:
// ✅ WORKS: SymbolIcon for built-in Windows symbols
var userIcon = new SymbolIcon(Symbol.Contact); // User silhouette
var searchIcon = new SymbolIcon(Symbol.Find); // Magnifying glass
var settingsIcon = new SymbolIcon(Symbol.Setting); // Gear
// Add to container
_avatarPlaceholder.Children.Add(userIcon);
Common useful symbols:
| Symbol | Description |
|---|---|
Symbol.Contact |
User silhouette |
Symbol.People |
Multiple users |
Symbol.Account |
User account |
Symbol.Find |
Magnifying glass |
Symbol.Setting |
Gear/settings |
Symbol.Add |
Plus sign |
Symbol.Delete |
Trash can |
Symbol.Edit |
Pencil |
Symbol.Favorite |
Star (outline) |
Symbol.SolidStar |
Star (filled) |
XAML usage:
<SymbolIcon Symbol="Contact" />
When to use which:
| Scenario | Use |
|---|---|
| Avatar placeholder (user icon) | SymbolIcon(Symbol.Contact) |
| Custom DaisyUI-specific icons | FloweryPathHelpers |
| Navigation arrows | FloweryPathHelpers.CreateChevron() |
| Generic UI icons (search, settings) | SymbolIcon |
19. UIElement can only have one parent - clear before re-parenting
In Uno Platform, a UIElement can only be the child of one parent at a time. Assigning a UIElement to a new parent while it's still attached to another throws ArgumentException: Value does not fall within the expected range.
// ❌ FAILS: Item is already a child of _nextPresenter
_nextPresenter.Content = Items[0];
_currentPresenter.Content = Items[0]; // Throws!
// ✅ WORKS: Clear the previous parent first
_nextPresenter.Content = null;
_currentPresenter.Content = Items[0];
Common scenarios:
- Swapping content between two
ContentPresenters (e.g., animation transitions) - Moving items between panels or containers
- Recycling UI elements across different parents
Very common trap (ContentControl that rebuilds its own visual tree):
If you capture a user's Content (which might be a UIElement) and then place it inside a new ContentPresenter, you MUST detach it first — otherwise the element temporarily has two parents (the original ContentControl + the new presenter) and Uno throws the same exception.
// ✅ Correct pattern for code-built visual trees in a ContentControl
private object? _userContent;
private ContentPresenter? _presenter;
private void OnLoaded(object sender, RoutedEventArgs e)
{
// Capture and DETACH before re-parenting
_userContent = Content;
Content = null;
BuildVisualTree();
_presenter!.Content = _userContent;
}
Also avoid this anti-pattern (it can reintroduce self-parenting / re-parenting issues when you later replace Content with your root):
// ❌ Anti-pattern: binding presenter back to this.Content in a control that replaces Content
_presenter.SetBinding(ContentPresenter.ContentProperty,
new Microsoft.UI.Xaml.Data.Binding { Source = this, Path = new PropertyPath("Content") });
Pattern for content swapping (see DaisyTextRotate.cs):
// Always clear both presenters before assigning
_nextPresenter.Content = null;
_currentPresenter.Content = null;
_currentPresenter.Content = Items[idx];
20. StaticResource scope in templates (merged dictionaries)
StaticResource resolves within the same ResourceDictionary (and its own merged dictionaries), not across Generic.xaml or app-level merges. If a template uses StaticResource for Geometry (e.g., icons), make sure the icon dictionary is merged into the same file or the resource won't be found at parse time.
<!-- GOOD: merge icon resources into the same dictionary that uses them -->
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="ms-appx:///Flowery.Uno/Themes/DaisyIcons.xaml" />
</ResourceDictionary.MergedDictionaries>
If you see XamlParseException for Path.Data, check that the geometry resources are in-scope.
21. {ThemeResource} does NOT auto-refresh when MergedDictionaries change
Critical difference from Avalonia: In Avalonia, {DynamicResource} automatically re-evaluates when resource dictionaries are swapped. In WinUI/Uno, {ThemeResource} only re-evaluates when Application.RequestedTheme toggles between Light and Dark — not when you swap MergedDictionaries or change resource values.
This means when you switch from one DaisyUI theme to another (e.g., "Dark" → "Dracula", both dark themes), elements using {ThemeResource DaisyBaseContentBrush} in XAML will not update their colors.
<!-- ❌ Will NOT update when theme changes (unless Light<->Dark toggles) -->
<TextBlock Foreground="{ThemeResource DaisyBaseContentBrush}" />
Solution: Subscribe to DaisyThemeManager.ThemeChanged and manually update properties:
public MyControl()
{
Loaded += OnLoaded;
Unloaded += OnUnloaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
DaisyThemeManager.ThemeChanged += OnThemeChanged;
ApplyTheme();
}
private void OnUnloaded(object sender, RoutedEventArgs e)
{
DaisyThemeManager.ThemeChanged -= OnThemeChanged;
}
private void OnThemeChanged(object? sender, string themeName)
{
ApplyTheme();
}
private void ApplyTheme()
{
var brush = DaisyResourceLookup.GetBrush("DaisyBaseContentBrush");
if (brush != null)
{
MyTextBlock.Foreground = brush;
}
}
For base classes like ScrollableExamplePage: You can enumerate the visual tree and update all TextBlocks that have a local Foreground value:
private void UpdateThemedForegrounds()
{
var brush = DaisyResourceLookup.GetBrush("DaisyBaseContentBrush");
if (brush == null) return;
foreach (var element in EnumerateVisualTree(this))
{
if (element is TextBlock textBlock &&
textBlock.ReadLocalValue(TextBlock.ForegroundProperty) != DependencyProperty.UnsetValue)
{
textBlock.Foreground = brush;
}
}
}
Note: The DaisyThemeManager.ApplyTheme() method mutates brush colors in-place via ApplyPaletteToApplicationResources(), which works for code-behind lookups but not for XAML {ThemeResource} bindings.
22. ContentControl XAML content capture timing issues
When inheriting from ContentControl and trying to capture XAML content (e.g., <MyControl>Some Text</MyControl>), timing issues can cause controls to be invisible or malfunction.
Problem: Capturing Content in OnLoaded and rebuilding the visual tree causes controls to become invisible when they have XAML content, while controls without content work fine.
Root cause: ContentControl's internal content handling conflicts with manual visual tree rebuilding when content is set via XAML. The sequence of:
- XAML sets
Content OnLoadedcaptures content- Code rebuilds visual tree and sets
Content = _rootGrid
...can cause rendering issues that are difficult to diagnose.
// ❌ FAILS: Controls with XAML content become invisible
public class MyDivider : ContentControl
{
private void OnLoaded(object sender, RoutedEventArgs e)
{
_userContent = Content; // Capture
RebuildVisualTree(); // Build
Content = _rootGrid; // Sets new content... but something breaks
}
}
Solution 1 (Recommended): Build visual tree ONCE in constructor, use dedicated properties for text content:
// ✅ WORKS: Build once in constructor, update dynamically
public class MyDivider : ContentControl
{
// Use a dedicated property instead of XAML content
public string? DividerText { get; set; }
public MyDivider()
{
DefaultStyleKey = typeof(MyDivider);
BuildVisualTree(); // Build ONCE in constructor
}
private void BuildVisualTree()
{
_rootGrid = new Grid { ... };
_textBlock = new TextBlock { ... };
_rootGrid.Children.Add(_textBlock);
Content = _rootGrid;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
// Just update existing elements, don't recreate
UpdateLayout();
ApplyColors();
}
}
XAML usage changes from <daisy:DaisyDivider>OR</daisy:DaisyDivider> to <daisy:DaisyDivider DividerText="OR" />.
Solution 2: If you must support XAML content, use OnContentChanged override:
// ⚠️ CAUTION: More complex, test thoroughly
protected override void OnContentChanged(object oldContent, object newContent)
{
base.OnContentChanged(oldContent, newContent);
// Skip our own grid to avoid infinite loop
if (newContent != null && !ReferenceEquals(newContent, _rootGrid))
{
_userContent = newContent as string ?? newContent?.ToString();
}
}
Key insight: Building the visual tree in the constructor (before any content is set) and using a dedicated property for text content completely avoids these timing issues.
23. CA1822 "can be made static" breaks x:Bind and XAML event handlers
Problem: The analyzer CA1822 suggests "Member does not access instance data and can be marked as static." However, applying this to properties or event handlers used in XAML breaks the build.
Root cause: The Uno XAML generator creates code like this.PropertyName or this.OnEventHandler(...). If those members are static, the generated code fails with CS0176: "Member cannot be accessed with an instance reference; qualify it with a type name instead."
// ❌ FAILS at build time (CS0176) - analyzer suggested static, but XAML needs instance
public static DateTime Today => DateTime.Today; // Used in x:Bind
public static bool IsFeatureEnabled => true; // Used in x:Bind
private static void OnButtonClick(object sender, RoutedEventArgs e) { } // XAML event
// ✅ WORKS - keep as instance members when used in XAML
public DateTime Today => DateTime.Today;
public bool IsFeatureEnabled => true;
private void OnButtonClick(object sender, RoutedEventArgs e) { }
Rule: IGNORE CA1822 for any member that is:
- Referenced in
{x:Bind PropertyName}in XAML - Used as a XAML event handler (e.g.,
Click="OnButtonClick")
The analyzer doesn't understand XAML bindings and gives incorrect suggestions for these cases.
📋 Porting Checklist (Quick Reference)
Before creating a new control file:
- Search for existing enums:
grep -r "enum EnumName" Controls/ - Check for helper classes: Look for EventArgs, converters, etc.
- Identify ambiguous types:
Path,Color,Brushoften need qualification - Review enum values: Ensure all values the new control needs are present
- Check existing Uno APIs:
Cursor,InputCursor, and WPF patterns may not exist
After creating the control:
- Build immediately:
dotnet buildto catch CS0101/CS0104 errors early - Fix duplicates by removing: Don't add
newkeyword, just delete the duplicate - Qualify ambiguous types: Use full namespace path
- Use correct FlowerySizeManager API:
GetIgnoreGlobalSize(this)not.IgnoreGlobalSize.GetValue()
⚠️ Known Limitations
Dashed Borders on Buttons (DaisyButtonStyle.Dash)
WinUI/Uno Button and Border do not natively support dashed borders. Unlike WPF which has BorderBrush with DashStyle, WinUI requires using a Rectangle or Path with StrokeDashArray overlaid on the control.
Current behavior: DaisyButtonStyle.Dash renders as Outline (solid border).
Future fix: Replace the button's border with a custom template containing a Rectangle:
<Rectangle Stroke="{TemplateBinding BorderBrush}"
StrokeThickness="1"
StrokeDashArray="4,2"
RadiusX="8" RadiusY="8" />
This requires template modifications in DaisyButton to use a ControlTemplate with a Grid containing both the dashed Rectangle and the ContentPresenter.
ClipToBounds Does Not Exist
WinUI/Uno Grid and Border do NOT have a ClipToBounds property (unlike WPF/Avalonia).
// ❌ FAILS: CS0117 - Border doesn't have ClipToBounds
_contentBorder = new Border
{
ClipToBounds = true
};
// ✅ WORKS: Remove ClipToBounds - WinUI handles clipping differently
// Use UIElement.Clip for explicit rectangle clipping if needed
_contentBorder = new Border
{
CornerRadius = new CornerRadius(8)
};
UIElement.Clip Only Accepts RectangleGeometry
Unlike WPF/Avalonia where Clip accepts any Geometry, WinUI/Uno only supports RectangleGeometry.
// ❌ FAILS: CS0266 - Cannot convert EllipseGeometry to RectangleGeometry
_border.Clip = new EllipseGeometry { ... };
_border.Clip = new PathGeometry { ... };
// ✅ WORKS: Only RectangleGeometry is allowed
_border.Clip = new RectangleGeometry { Rect = new Rect(0, 0, w, h) };
Workarounds for non-rectangular clipping:
- Use Windows Composition API's
CompositionGeometricClip - Use opacity masks with image brushes
- Use a
Pathelement as a visual overlay mask
RectangleGeometry Has No RadiusX/RadiusY
WinUI's RectangleGeometry does NOT support rounded corners via RadiusX/RadiusY properties.
// ❌ FAILS: CS0117 - RectangleGeometry has no RadiusX/RadiusY
var geo = new RectangleGeometry
{
Rect = new Rect(0, 0, w, h),
RadiusX = 8,
RadiusY = 8
};
// ✅ WORKS: Use PathGeometry with Bezier curves for rounded corners
// Or use Border.CornerRadius for visual rounding (not clipping)
ToolTipService.SetToolTip() Required
Controls don't have a direct .ToolTip property. Use ToolTipService.
// ❌ FAILS: CS0117 - No ToolTip property
var kbd = new DaisyKbd { ToolTip = "Press here" };
// ✅ WORKS: Use ToolTipService
var kbd = new DaisyKbd();
ToolTipService.SetToolTip(kbd, "Press here");
Grid.SetColumn() Requires FrameworkElement
Grid.SetColumn(), Grid.SetRow(), etc. require FrameworkElement, not UIElement.
// ❌ FAILS: CS1503 - Cannot convert UIElement to FrameworkElement
UIElement element = GetElement();
Grid.SetColumn(element, 0);
// ✅ WORKS: Cast to FrameworkElement first
if (element is FrameworkElement fe)
{
Grid.SetColumn(fe, 0);
}
_grid.Children.Add(element);
🔧 Build Script
⚠️ IF the user instructs to build anything, use build_all.ps1 or build_all.sh to build Flowery.Uno. Do NOT run dotnet build directly.
# From Git Bash:
./scripts/build_all.sh
# Or from PowerShell:
.\scripts\build_all.ps1
The script handles:
- Proper build configuration
- Detailed logging to
all_build.log - Error/warning extraction for quick feedback
- Consistent build environment
Why not dotnet build directly?
- Context pollution with thousands of lines
- Missing proper logging configuration
- Inconsistent output formatting
- No automatic error parsing
- May miss Uno-specific build requirements
✅ Porting Workflow Checklist
For the step-by-step, repeatable “do this every time” workflow, see:
!todos\PORTING_CHECKLIST.md
📖 Useful Resources
- Uno Platform Docs
- Composition API in Uno
- WinUI Controls Reference
- Lottie Animations - For complex vector animations