diff --git a/src/Avalonia.Controls/Design.cs b/src/Avalonia.Controls/Design.cs index 80600b2276..9d6bb93ebb 100644 --- a/src/Avalonia.Controls/Design.cs +++ b/src/Avalonia.Controls/Design.cs @@ -1,102 +1,433 @@ - +using System; using System.Collections.Generic; -using System.Runtime.CompilerServices; +using Avalonia.Controls.Templates; +using Avalonia.Layout; +using Avalonia.Metadata; using Avalonia.Styling; namespace Avalonia.Controls { + /// + /// Provides attached properties and helpers for design-time support. + /// public static class Design { - private static Dictionary? _previewWith; + private static Dictionary?> s_previewWith = []; + private static Dictionary s_templateDataContext = []; + /// + /// Gets a value indicating whether the application is running in design mode. + /// + /// + /// This property is typically used to enable or disable features that should only be available + /// at design-time, such as sample/preview data. + /// public static bool IsDesignMode { get; internal set; } + /// + /// Defines the Height attached property. + /// public static readonly AttachedProperty HeightProperty = AvaloniaProperty .RegisterAttached("Height", typeof (Design)); + /// + /// Sets the design-time height for a control. + /// + /// The control to set the height for. + /// The height value. public static void SetHeight(Control control, double value) { control.SetValue(HeightProperty, value); } + /// + /// Gets the design-time height for a control. + /// + /// The control to get the height from. + /// The height value. public static double GetHeight(Control control) { return control.GetValue(HeightProperty); } + /// + /// Defines the Width attached property. + /// public static readonly AttachedProperty WidthProperty = AvaloniaProperty .RegisterAttached("Width", typeof(Design)); + /// + /// Sets the design-time width for a control. + /// + /// The control to set the width for. + /// The width value. public static void SetWidth(Control control, double value) { control.SetValue(WidthProperty, value); } + /// + /// Gets the design-time width for a control. + /// + /// The control to get the width from. + /// The width value. public static double GetWidth(Control control) { return control.GetValue(WidthProperty); } - public static readonly AttachedProperty DataContextProperty = AvaloniaProperty - .RegisterAttached("DataContext", typeof (Design)); + /// + /// Defines the DataContext attached property. + /// + public static readonly AttachedProperty DataContextProperty = AvaloniaProperty + .RegisterAttached("DataContext", typeof (Design)); - public static void SetDataContext(Control control, object value) + /// + /// Sets the design-time data context for a control. + /// + /// The control to set the data context for. + /// The data context value. + public static void SetDataContext(Control control, object? value) { control.SetValue(DataContextProperty, value); } - public static object GetDataContext(Control control) + /// + /// Gets the design-time data context for a control. + /// + /// The control to get the data context from. + /// The data context value. + public static object? GetDataContext(Control control) { return control.GetValue(DataContextProperty); } - + + /// + /// Sets the design-time data context for a control. + /// + /// The control to set the data context for. + /// The data context value. + public static void SetDataContext(IDataTemplate control, object? value) + { + s_templateDataContext[control] = value; + } + + /// + /// Gets the design-time data context for a control. + /// + /// The control to get the data context from. + /// The data context value. + public static object? GetDataContext(IDataTemplate control) + { + return s_templateDataContext.TryGetValue(control, out var value) ? value : null; + } + + /// + /// Defines the PreviewWith attached property. + /// public static readonly AttachedProperty PreviewWithProperty = AvaloniaProperty .RegisterAttached("PreviewWith", typeof (Design)); + /// + /// Sets a preview template for the specified at design-time. + /// + /// + /// This method allows you to specify a substitute control to be rendered in the previewer + /// for a given object. + /// + /// The target object. + /// The preview control. + // TODO12: Remove this overload in Avalonia 12 + [Obsolete("Use SetPreviewWith(AvaloniaObject, ITemplate) overload instead. Use from XAML")] public static void SetPreviewWith(AvaloniaObject target, Control? control) { - target.SetValue(PreviewWithProperty, control); + s_previewWith[target] = control is not null ? new FuncTemplate(() => control) : null; + } + + /// + /// Sets a preview template for the specified at design-time. + /// + /// + /// This method allows you to specify a substitute control template to be rendered in the previewer + /// for a given object. + /// + /// The target object. + /// The preview template. + public static void SetPreviewWith(AvaloniaObject target, ITemplate? template) + { + s_previewWith[target] = template; } + /// + /// Sets a preview template for the specified at design-time. + /// + /// + /// This method allows you to specify a substitute control template to be rendered in the previewer. + /// ResourceDictionary is attached to that control, displaying real time changes on the control. + /// + /// The resource dictionary. + /// The preview template. + public static void SetPreviewWith(ResourceDictionary target, ITemplate? template) + { + s_previewWith[target] = template; + } + + /// + /// Sets a preview template for the specified at design-time. + /// + /// + /// This method allows you to specify a substitute control to be rendered in the previewer. + /// ResourceDictionary is attached to that control, displaying real time changes on the control. + /// + /// The resource dictionary. + /// The preview control. public static void SetPreviewWith(ResourceDictionary target, Control? control) { - _previewWith ??= new(); - _previewWith[target] = control; + s_previewWith[target] = control is not null ? new FuncTemplate(() => control) : null; + } + + /// + /// Sets a preview template for the specified at design-time. + /// + /// + /// This method allows you to specify a substitute control template to be rendered in the previewer. + /// Template must return ContentControl, and IDataTemplate will be set assigned to ContentControl.ContentTemplate property. + /// + /// The data template. + /// The preview template. + public static void SetPreviewWith(IDataTemplate target, ITemplate? template) + { + s_previewWith[target] = template is not null ? new FuncTemplate(template.Build) : null; + } + + /// + /// Sets a preview template for the specified at design-time. + /// + /// + /// This method allows you to specify a substitute control to be rendered in the previewer. + /// Template must return ContentControl, and IDataTemplate will be set assigned to ContentControl.ContentTemplate property. + /// + /// The data template. + /// The preview control. + public static void SetPreviewWith(IDataTemplate target, Control? control) + { + s_previewWith[target] = control is not null ? new FuncTemplate(() => control) : null; + } + + + /// + /// Sets a preview template for the specified at design-time. + /// + /// + /// This method allows you to specify a substitute control template to be rendered in the previewer. + /// Template must return ContentControl, and IDataTemplate will be set assigned to ContentControl.ContentTemplate property. + /// + /// The data template. + /// The preview template. + public static void SetPreviewWith(IStyle target, ITemplate? template) + { + s_previewWith[target] = template is not null ? new FuncTemplate(template.Build) : null; + } + + + /// + /// Sets a preview template for the specified at design-time. + /// + /// + /// This method allows you to specify a substitute control to be rendered in the previewer. + /// Template must return ContentControl, and IDataTemplate will be set assigned to ContentControl.ContentTemplate property. + /// + /// The data template. + /// The preview control. + public static void SetPreviewWith(IStyle target, Control? control) + { + s_previewWith[target] = control is not null ? new FuncTemplate(() => control) : null; } + + /// + /// Gets the preview control for the specified at design-time. + /// + /// The target object. + /// The preview control, or null. public static Control? GetPreviewWith(AvaloniaObject target) { - return target.GetValue(PreviewWithProperty); + return s_previewWith.TryGetValue(target, out var template) ? template?.Build() : null; } + /// + /// Gets the preview control for the specified at design-time. + /// + /// The resource dictionary. + /// The preview control, or null. public static Control? GetPreviewWith(ResourceDictionary target) { - return _previewWith?[target]; + return s_previewWith.TryGetValue(target, out var template) ? template?.Build() : null; + } + + /// + /// Gets the preview control for the specified at design-time. + /// + /// The data template. + /// The preview control, or null. + public static Control? GetPreviewWith(IDataTemplate target) + { + return s_previewWith.TryGetValue(target, out var template) ? template?.Build() : null; + } + + /// + /// Gets the preview control for the specified at design-time. + /// + /// The style. + /// The preview control, or null. + public static Control? GetPreviewWith(IStyle target) + { + return s_previewWith.TryGetValue(target, out var template) ? template?.Build() : null; } + /// + /// Identifies the DesignStyle attached property for design-time use. + /// + /// + /// This property allows you to apply a style to a control only at design-time, enabling + /// custom visualizations or highlighting in the designer without affecting the runtime appearance. + /// public static readonly AttachedProperty DesignStyleProperty = AvaloniaProperty .RegisterAttached("DesignStyle", typeof(Design)); + /// + /// Sets the design-time style for a control. + /// + /// The control to set the style for. + /// The style value. public static void SetDesignStyle(Control control, IStyle value) { control.SetValue(DesignStyleProperty, value); } + /// + /// Gets the design-time style for a control. + /// + /// The control to get the style from. + /// The style value. public static IStyle GetDesignStyle(Control control) { return control.GetValue(DesignStyleProperty); } + [PrivateApi] public static void ApplyDesignModeProperties(Control target, Control source) { if (source.IsSet(WidthProperty)) - target.Width = source.GetValue(WidthProperty); + target.Bind(Layoutable.WidthProperty, target.GetBindingObservable(WidthProperty)); if (source.IsSet(HeightProperty)) - target.Height = source.GetValue(HeightProperty); + target.Bind(Layoutable.HeightProperty, target.GetBindingObservable(HeightProperty)); if (source.IsSet(DataContextProperty)) - target.DataContext = source.GetValue(DataContextProperty); + target.Bind(StyledElement.DataContextProperty, target.GetBindingObservable(DataContextProperty)); if (source.IsSet(DesignStyleProperty)) - target.Styles.Add(source.GetValue(DesignStyleProperty)); + target.Styles.Add(GetDesignStyle(source)); + } + + [PrivateApi] + public static Control CreatePreviewWithControl(object target) + { + if (target is IStyle style) + { + var substitute = GetPreviewWith((AvaloniaObject)style); + if (substitute != null) + { + substitute.Styles.Add(style); + return substitute; + } + + return new StackPanel + { + Children = + { + new TextBlock {Text = "Styles can't be previewed without Design.PreviewWith. Add"}, + new TextBlock {Text = ""}, + new TextBlock {Text = " "}, + new TextBlock {Text = ""}, + new TextBlock {Text = "before setters in your first Style"} + } + }; + } + + if (target is ResourceDictionary resources) + { + var substitute = GetPreviewWith(resources); + if (substitute != null) + { + substitute.Resources.MergedDictionaries.Add(resources); + return substitute; + } + + return new StackPanel + { + Children = + { + new TextBlock {Text = "ResourceDictionaries can't be previewed without Design.PreviewWith. Add"}, + new TextBlock {Text = ""}, + new TextBlock {Text = " "}, + new TextBlock {Text = ""}, + new TextBlock {Text = "in your resource dictionary"} + } + }; + } + + if (target is IDataTemplate template) + { + if (GetPreviewWith(template) is ContentControl substitute) + { + substitute.ContentTemplate = template; + if (!substitute.IsSet(DataContextProperty) && substitute.IsSet(StyledElement.DataContextProperty)) + { + substitute.DataContext = substitute.GetValue(StyledElement.DataContextProperty); + } + return substitute; + } + + if (GetDataContext(template) is { } dataContext) + { + substitute = new ContentControl + { + ContentTemplate = template, + DataContext = dataContext, + Content = dataContext + }; + return substitute; + } + + return new StackPanel + { + Children = + { + new TextBlock {Text = "IDataTemplate can't be previewed without Design.PreviewWith."}, + new TextBlock {Text = "Provide ContentControl with your design data as Content. Previewer will set ContentTemplate from this file."}, + new TextBlock {Text = ""}, + new TextBlock {Text = " "}, + new TextBlock {Text = ""} + } + }; + } + + if (target is Application) + { + return new TextBlock { Text = "This file cannot be previewed in design view" }; + } + + if (target is AvaloniaObject avObject and not Window + && GetPreviewWith(avObject) is { } previewWith) + { + return previewWith; + } + + if (target is not Control control) + { + return new TextBlock { Text = "This file cannot be previewed in design view" }; + } + + return control; } } } diff --git a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs index 6395529278..df6d7abd4d 100644 --- a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs +++ b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs @@ -6,6 +6,7 @@ using System.Text; using Avalonia.Controls; using Avalonia.Controls.Embedding.Offscreen; using Avalonia.Controls.Platform; +using Avalonia.Controls.Templates; using Avalonia.Markup.Xaml; using Avalonia.Styling; @@ -19,7 +20,6 @@ namespace Avalonia.DesignerSupport public static Window LoadDesignerWindow(string xaml, string assemblyPath, string xamlFileProjectPath, double renderScaling) { Window window; - Control control; using (PlatformManager.DesignerMode()) { var loader = AvaloniaLocator.Current.GetRequiredService(); @@ -45,60 +45,9 @@ namespace Avalonia.DesignerSupport DesignMode = true, UseCompiledBindingsByDefault = bool.TryParse(useCompiledBindings, out var parsedValue) && parsedValue }); - var style = loaded as IStyle; - var resources = loaded as ResourceDictionary; - if (style != null) - { - var substitute = Design.GetPreviewWith((AvaloniaObject)style); - if (substitute != null) - { - substitute.Styles.Add(style); - control = substitute; - } - else - control = new StackPanel - { - Children = - { - new TextBlock {Text = "Styles can't be previewed without Design.PreviewWith. Add"}, - new TextBlock {Text = ""}, - new TextBlock {Text = " "}, - new TextBlock {Text = ""}, - new TextBlock {Text = "before setters in your first Style"} - } - }; - } - else if (resources != null) - { - var substitute = Design.GetPreviewWith(resources); - if (substitute != null) - { - substitute.Resources.MergedDictionaries.Add(resources); - control = substitute; - } - else - control = new StackPanel - { - Children = - { - new TextBlock {Text = "ResourceDictionaries can't be previewed without Design.PreviewWith. Add"}, - new TextBlock {Text = ""}, - new TextBlock {Text = " "}, - new TextBlock {Text = ""}, - new TextBlock {Text = "in your resource dictionary"} - } - }; - } - else if (loaded is Application) - control = new TextBlock { Text = "This file cannot be previewed in design view" }; - else - control = (Control)loaded; - window = control as Window; - if (window == null) - { - window = new Window() { Content = (Control)control }; - } + var control = Design.CreatePreviewWithControl(loaded); + window = control as Window ?? new Window { Content = control }; if (window.PlatformImpl is OffscreenTopLevelImplBase offscreenImpl) offscreenImpl.RenderScaling = renderScaling; diff --git a/tests/Avalonia.Controls.UnitTests/DesignTests.cs b/tests/Avalonia.Controls.UnitTests/DesignTests.cs new file mode 100644 index 0000000000..6845e4d369 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/DesignTests.cs @@ -0,0 +1,149 @@ +using Avalonia.Controls.Templates; +using Avalonia.LogicalTree; +using Avalonia.Markup.Xaml.MarkupExtensions; +using Avalonia.Media; +using Avalonia.UnitTests; +using Avalonia.Styling; +using System; +using System.Linq; +using Avalonia.Controls.Primitives; +using Xunit; + +namespace Avalonia.Controls.UnitTests; + +public class DesignTests : ScopedTestBase +{ + [Fact] + public void Should_Preview_Resource_Dictionary_With_Template() + { + using var _ = UnitTestApplication.Start(TestServices.StyledWindow); + + var dictionary = new ResourceDictionary { ["TestColor"] = Colors.Green }; + Design.SetPreviewWith(dictionary, + new FuncTemplate(static () => + new Border { [!Border.BackgroundProperty] = new DynamicResourceExtension("TestColor") })); + + var preview = Design.CreatePreviewWithControl(dictionary); + + var border = Assert.IsType(preview); + Assert.Equal(Colors.Green, ((ISolidColorBrush)border.Background!).Color); + } + + [Fact] + public void Should_Preview_DataTemplate_With_ContentControl() + { + using var _ = UnitTestApplication.Start(TestServices.StyledWindow); + + const string testData = "Test Data"; + var dataTemplate = new FuncDataTemplate((data, _) => + new TextBlock { Text = data }); + Design.SetPreviewWith(dataTemplate, + new FuncTemplate(static () => new ContentControl { Content = testData })); + + var preview = Design.CreatePreviewWithControl(dataTemplate); + + var previewContentControl = Assert.IsType(preview); + Assert.Equal(testData, previewContentControl.Content); + Assert.Same(dataTemplate, previewContentControl.ContentTemplate); + } + + [Fact] + public void Should_Preview_DataTemplate_With_DataContext() + { + using var _ = UnitTestApplication.Start(TestServices.StyledWindow); + + const string testData = "Test Data"; + var dataTemplate = new FuncDataTemplate((data, _) => + new TextBlock { Text = data }); + Design.SetDataContext(dataTemplate, testData); + + var preview = Design.CreatePreviewWithControl(dataTemplate); + + var previewContentControl = Assert.IsType(preview); + Assert.Equal(testData, previewContentControl.Content); + Assert.Same(dataTemplate, previewContentControl.ContentTemplate); + } + + [Fact] + public void Should_Preview_Control_With_Another_Control() + { + using var _ = UnitTestApplication.Start(TestServices.StyledWindow); + + var control = new TextBlock(); + Design.SetPreviewWith(control, + new FuncTemplate(static () => new Border())); + + var preview = Design.CreatePreviewWithControl(control); + + Assert.IsType(preview); + } + + [Fact] + public void Should_Apply_Design_Mode_Properties() + { + using var _ = UnitTestApplication.Start(TestServices.StyledWindow); + + var control = new ContentControl(); + + Design.SetWidth(control, 200); + Design.SetHeight(control, 150); + Design.SetDataContext(control, "TestDataContext"); + Design.SetDesignStyle(control, + new Style(x => x.OfType()) + { + Setters = { new Setter(TemplatedControl.BackgroundProperty, Brushes.Yellow) } + }); + + Design.ApplyDesignModeProperties(control, control); + + Assert.Equal(200, control.Width); + Assert.Equal(150, control.Height); + Assert.Equal("TestDataContext", control.DataContext); + Assert.Contains(control.Styles, + s => ((Style)s).Setters.OfType().First().Property == TemplatedControl.BackgroundProperty); + } + + [Fact] + public void Should_Not_Throw_Exception_On_Generic_Style() + { + using var _ = UnitTestApplication.Start(TestServices.StyledWindow); + + var preview = Design.CreatePreviewWithControl(new Style(x => x.OfType", designMode: false); + var preview = Design.CreatePreviewWithControl(obj); + // Should return the original control, not the preview. + Assert.IsType", designMode: true); + var preview = Design.CreatePreviewWithControl(obj); + var previewBorder = Assert.IsType(preview); + Assert.IsType