如何将元数据与 WPF 中的许多不同类型的控件相关联?

How to associate metadata with many different types of controls in WPF?

我有一堆控件可能会或可能不会根据其他控件中的错误启用。当控件被禁用时,我想向用户解释为什么它当前被禁用。但是,我已经在使用 ToolTip 来获取帮助文本。所以我想当用户将鼠标悬停在灰色组件上时,我会在静态文本框中显示错误消息。

简化xaml:

<StackPanel Margin="10 10 10 10">
    <TextBox x:Name="thtkDirTextBox" ToolTip="yadda yadda help text"/>
    <TextBox x:Name="tououDatTextBox" ToolTip="yadda yadda help text"/>
    <ComboBox x:Name="touhouExeSelector" ToolTip="yadda yadda help text"/>
    <ComboBox x:Name="stageSelector" ToolTip="yadda yadda help text"/>
    <ComboBox x:Name="sceneSelector" ToolTip="yadda yadda help text"/>
    <GroupBox Header="Errors">
        <TextBox IsEnabled="False" TextWrapping="Wrap" Width="200" FontSize="10" Height="50">
            Mouse over an item that's grayed out and the reason why will appear here.
        </TextBox>
    </GroupBox>
    <StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
        <Button Content="Save as..." ToolTip="yadda yadda help text"></Button>
        <Button Content="Quick Play" ToolTip="yadda yadda help text"></Button>
    </StackPanel>
</StackPanel>

至关重要的是,为了使其正常工作,每个组件都需要与自定义的元数据片段相关联以保存其错误消息。 (不同控件变灰的原因可能不同!)

我阅读了有关 DependencyProperties 的内容,但似乎使用它们需要对控件进行子类化。如果我必须创建 TextBoxComboBoxButton 的子类,那么我最终会在三个不同的不兼容的 DependencyProperties 中得到重复的代码。 (另外,几乎所有的例子都涉及触发器的使用,我认为这对我没有帮助?)

我看到还有一个Tag属性。这是 属性 的合理用法吗?如果我后来发现我需要另一个标签 属性 怎么办?我是否应该假设这可能不会发生? (或者如果最终确实如此,也许将 JSON 对象序列化到 Tag 中以存储多个属性也许不是不合理的?)

我应该在这里做什么?


其他详细信息:

我正在使用 ReactiveUI 将信息从一个控件传播到另一个控件。对于每个可以变灰的控件,ViewModel 最终构造一个 IObservable ,其项目要么是控件所需的某些值(例如,一个 IEnumerable<string> 带有组合框的选项,或者可能只是一个 Unit 作为按钮有效性的证明)或错误消息。

给定这些可观察量之一,我可以轻松地 OneWayBind 类型 boolObservableAsPropertyHelper 到控件的 IsEnabled 属性,并希望这样做错误消息类似的东西。如果我为此使用工具提示,那将相当简单:

// ViewModel
this.stageSelectorErrorMessage =
    this.stageSelectorEithers
    .Select(either => either.Error) // string on failure, null on success
    .ScheduleOn(RxApp.MainThreadScheduler)
    .ToProperty(this, x => x.StageSelectorErrorMessage);

// View
this.WhenActivated(disposableRegistration => {
    // ...
    this.OneWayBind(this.ViewModel,
        viewModel => viewModel.StageSelectorErrorMessage,
        view => view.stageSelector.ToolTip
    ).DisposeWith(disposableRegistration);
})

我还没有决定如何连接鼠标悬停事件。 (还没到那一步)

I see there is also a Tag property. Is this a fair usage of the property?

更好的方法是创建一个可以在任何元素上设置的强类型 attached dependency property。只需在注册 属性:

的地方定义一个静态 class
public static class Metadata
{
    public static readonly DependencyProperty InfoProperty = DependencyProperty.RegisterAttached(
          "Info",
          typeof(string),
          typeof(Metadata),
          new FrameworkPropertyMetadata(null));

    public static void SetInfo(UIElement element, string value) => element.SetValue(InfoProperty, value);

    public static string GetInfo(UIElement element) => (string)element.GetValue(InfoProperty);
}

您可以像这样在任何 UIElement 上设置它:

<TextBox IsEnabled="False" TextWrapping="Wrap" Width="200" FontSize="10" Height="50"
             local:Metadata.Info="Mouse over an item that's grayed out and the reason why will appear here.">
</TextBox>

您使用 GetInfo 方法以编程方式获取值:

string info = Metadata.GetInfo(theControl);