跳到主要内容
版本:11.0.0

如何创建高级自定义控件

从自定义控件指南中摘录的内容。

这是Border控件如何定义其Background属性的方式:

AvaloniaProperty.Register方法还接受其他一些参数:

  • defaultValue:为属性设置默认值。请确保只传递值类型和不可变类型,因为传递引用类型将导致所有注册了该属性的实例使用同一个对象。
  • inherits:指定属性的默认值应来自父控件。
  • defaultBindingMode:属性的默认绑定模式。可以设置为OneWayTwoWayOneTimeOneWayToSource
  • validate:一个类型为Func<TOwner, TValue, TValue>的验证/强制函数。该函数接受正在设置属性的类的实例和值,并返回强制后的值,或者对于无效值抛出异常。

一个样式化属性类似于其他XAML框架中的DependencyProperty

属性的命名约定及其对应的AvaloniaProperty字段的命名是重要的。字段的名称始终是属性的名称,后面附加了Property后缀。

在另一个类上使用StyledProperty

有时,您想要添加到自定义控件的属性已经存在于另一个控件上,Background就是一个很好的例子。要注册在另一个控件上定义的属性,您需要调用StyledProperty.AddOwner

public static readonly StyledProperty<IBrush> BackgroundProperty =
Border.BackgroundProperty.AddOwner<Panel>();

public Brush Background
{
get { return GetValue(BackgroundProperty); }
set { SetValue(BackgroundProperty, value); }
}

注意:与WPF/UWP不同,属性必须在类上注册,否则无法在该类的对象上设置属性。但这可能会在将来发生改变。

只读属性

要创建一个只读属性,您可以使用AvaloniaProperty.RegisterDirect方法。以下是Visual如何注册只读的Bounds属性:

public static readonly DirectProperty<Visual, Rect> BoundsProperty =
AvaloniaProperty.RegisterDirect<Visual, Rect>(
nameof(Bounds),
o => o.Bounds);

private Rect _bounds;

public Rect Bounds
{
get { return _bounds; }
private set { SetAndRaise(BoundsProperty, ref _bounds, value); }
}

可以看到,只读属性被存储为对象的字段。在注册属性时,传递了一个getter,用于通过GetValue访问属性值,然后使用SetAndRaise通知属性更改的监听器。

附加属性

附加属性的定义与样式化属性几乎相同,只是它们使用RegisterAttached方法进行注册,并且它们的访问器被定义为静态方法。

以下是Grid如何定义其Grid.Column附加属性:

public static readonly AttachedProperty<int> ColumnProperty =
AvaloniaProperty.RegisterAttached<Grid, Control, int>("Column");

public static int GetColumn(Control element)
{
return element.GetValue(ColumnProperty);
}

public static void SetColumn(Control element, int value)
{
element.SetValue(ColumnProperty, value);
}

直接的Avalonia属性

顾名思义,RegisterDirect不仅用于注册只读属性。您还可以将一个_setter_传递给RegisterDirect,将标准的C#属性公开为Avalonia属性。

使用AvaloniaProperty.Register注册的StyledProperty维护了一个优先级列表,其中包含允许样式工作的值和绑定。然而,对于许多属性来说,这是不必要的,比如ItemsControl.Items——它永远不会被样式化,使用样式化属性的开销是不必要的。

以下是ItemsControl.Items的注册方式:

public static readonly DirectProperty<ItemsControl, IEnumerable> ItemsProperty =
AvaloniaProperty.RegisterDirect<ItemsControl, IEnumerable>(
nameof(Items),
o => o.Items,
(o, v) => o.Items = v);

private IEnumerable _items = new AvaloniaList<object>();

public IEnumerable Items
{
get { return _items; }
set { SetAndRaise(ItemsProperty, ref _items, value); }
}

直接属性是样式化属性的轻量级版本,支持以下功能:

  • AvaloniaObject.GetValue
  • AvaloniaObject.SetValue(非只读属性)
  • PropertyChanged
  • Binding(仅具有LocalValue优先级)
  • GetObservable
  • AddOwner
  • Metadata

它们不支持以下功能:

  • 验证/强制(尽管可以在属性setter中完成)
  • 覆盖默认值。
  • 继承的值

在另一个类上使用DirectProperty

与样式化属性一样,您可以在直接属性上调用AddOwner来添加一个所有者。由于直接属性引用控件上的字段,因此您还必须为该属性添加一个字段:

public static readonly DirectProperty<MyControl, IEnumerable> ItemsProperty =
ItemsControl.ItemsProperty.AddOwner<MyControl>(
o => o.Items,
(o, v) => o.Items = v);

private IEnumerable _items = new AvaloniaList<object>();

public IEnumerable Items
{
get { return _items; }
set { SetAndRaise(ItemsProperty, ref _items, value); }
}

何时使用Direct属性和Styled属性

通常情况下,应将属性声明为样式化属性。但是,直接属性具有优点和缺点:

优点:

  • 每个实例不需要额外的对象来存储属性
  • 属性getter是标准的C#属性getter
  • 属性setter是引发事件的标准C#属性setter
  • 您可以添加数据验证支持

缺点:

  • 无法从父控件继承值
  • 无法利用Avalonia的样式系统
  • 属性值是一个字段,因此无论属性是否在对象上设置,都会被分配内存

因此,当满足以下要求时,请使用直接属性:

  • 属性不需要样式化
  • 属性通常或总是具有值

数据验证支持

如果要允许属性验证数据并显示验证错误消息,则该属性必须实现为DirectProperty,并且必须启用验证支持(enableDataValidation: true)。

启用数据验证的属性示例

public static readonly DirectProperty<MyControl, int> ValueProperty =
AvaloniaProperty.RegisterDirect<MyControl, int>(
nameof(Value),
o => o.Value,
(o, v) => o.Value = v,
enableDataValidation: true);

如果要重用另一个类的直接属性,也可以启用数据验证。在这种情况下,请使用AddOwnerWithDataValidation

示例:TextBox.TextProperty属性重用TextBlock.TextProperty,但添加了验证支持

public static readonly DirectProperty<TextBox, string?> TextProperty =
TextBlock.TextProperty.AddOwnerWithDataValidation<TextBox>(
o => o.Text,
(o, v) => o.Text = v,
defaultBindingMode: BindingMode.TwoWay,
enableDataValidation: true);