在WPF/Silverlight页面中设置自定义属性

46

听起来应该很简单。我在XAML中按照正常方式(即使用“添加新项...”)声明了一个Page,并且它有一个自定义属性。我想在与页面关联的XAML中设置该属性。

尝试以与设置任何其他属性相同的方式执行此操作不起作用,出于我理解但不知道如何解决的原因。为了让我们有一些具体的谈论内容,这里是一些(无效的)XAML。我已经尽可能地将所有内容缩减了-最初有设计师大小等属性,但我相信这些属性与我所要做的无关。

<Page x:Class="WpfSandbox.TestPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      MyProperty="MyPropertyValue">
</Page>

以及相应的代码后台:

using System.Windows.Controls;

namespace WpfSandbox {
  public partial class TestPage : Page {
    public TestPage() {
      InitializeComponent();
    }

    public string MyProperty { get; set; }
  }
}

错误信息:

Error 1:属性 'MyProperty' 在 XML 命名空间'http://schemas.microsoft.com/winfx/2006/xaml/presentation'中不存在。第4行第7个位置。

现在我知道为什么会失败了:元素的类型是 Page,而 Page 没有一个名为 MyProperty 的属性。 这个属性只在 TestPage 中声明...它是由 x:Class 属性指定的,而不是元素本身指定的。据我所知,这种配置是 XAML 处理模型所需的(例如 Visual Studio 集成等)。

我怀疑我可以通过依赖属性来处理这个问题,但那感觉有点大题小做。我也可以使用现有属性(例如 DataContext),然后在代码中将其值复制到自定义属性中,但那样会很难看。

上面是一个 WPF 示例,但我认为相同的答案也适用于 Silverlight。我对两者都感兴趣 - 所以如果您发布一个您知道只适用于其中一种的答案,我会非常感激您在答案中指出 :)

当有人发布一个绝对简单的解决方案时,我准备踢自己的后腿...


在 xaml 的 Page 元素中,"MyProperty" 是否需要一个 xml 命名空间?例如 "x:MyProperty"?(不是字面意思,但类似的)。关键在于它不是在该命名空间中,因此它会检查哪些其他命名空间? - Neil Barnwell
1
@Filip:我不相信这实际上是那个问题的复制,那个问题是关于附加属性的。这里的问题是我正在尝试设置的属性实际上是实际类的属性,而不是元素声明的属性。当然,我可能是错的。 - Jon Skeet
4
哇..Jon Skeet 竟然只差一点就被否决了!这个世界要怎么样才好啊? - Arcturus
赶紧回答Jon Skeet的问题!我一段时间前也遇到了完全相同的问题,这个问题帮了我一点忙:https://dev59.com/1HVC5IYBdhLWcg3wpS3g - Chris S
你为什么不能在构造函数中设置属性的值呢? - Quartermeister
@Quartermeister:只是这样做会稍微有点丑陋。我从静态资源中获取值,这样以声明方式实现更好看。 - Jon Skeet
10个回答

33

如果您为页面创建一个基类,那么您可以使用普通属性而不需要依赖属性。

public class BaseWindow : Window
{
   public string MyProperty { get; set; }
}
<local:BaseWindow x:Class="BaseWindowSample.Window1" x:Name="winImp"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:BaseWindowSample" 
    MyProperty="myproperty value"
    Title="Window1" Height="300" Width="300">

</local:BaseWindow>

即使MyProperty不是一个依赖属性或附加属性,它仍然能正常工作。


@Jon,这是Håvard所追求的相同方法。你将失去InitializeComponent()。请参阅他帖子中的评论。你应该在Window1中有InitializeComponent()。然而,使用依赖属性似乎更加清晰明了,更有意义。 - Filip Ekberg
3
@Filip: 看起来你的方法和Håvard的不一样 - 我仍然不太清楚你所说的“失去” InitializeComponent() 是指哪个类?(这里涉及到两个类。)它在派生类中仍然存在,并且似乎能够工作。 - Jon Skeet
@Jon:你说得对。对于没有太多命名元素的情况,可以修改Håvard的方法并自己实现InitializeComponent()。然而,在我看来,从设计师那里获得更好的支持的好处胜过使用一个小基类shim来保存属性的成本。 - AnthonyWJones
@Chris:x:Class 会创建一个部分类,其中包含一个动态创建的 InitializeComponent 方法。在该方法中,有一个对 LoadComponent 的调用,它将 Xaml 应用于正在构建的页面。正是这个 LoadComponent 将在页面中声明的事件附加到 Page 中定义的适当事件方法。请参见下面我的答案。 - AnthonyWJones
@Anthony 是正确的。如果你声明了一个 x:Class,那么隐藏的部分类(whatever.xaml.g.cs)将会被生成...是编译器还是 WPF 项目系统完成这个过程我不太确定。整个过程中有太多的魔法元素,我的看法是如此。 - user1228
显示剩余16条评论

6

正如Pavel所指出的那样,您需要将其变成可附加属性,然后您可以编写以下内容:

<Page x:Class="JonSkeetTest.SkeetPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:JonSkeetTest="clr-namespace:JonSkeetTest" mc:Ignorable="d" 
      d:DesignHeight="300" d:DesignWidth="300"
       JonSkeetTest:SkeetPage.MyProperty="testar"
    Title="SkeetPage">
    <Grid>
        
    </Grid>
</Page>

然而,仅有这份代码就会导致以下错误:

在类型“SkeetPage”中未找到可附加的属性“MyProperty”。

附加属性“SkeetPage.MyProperty”未在“Page”或其基类之一上定义。


编辑

不幸的是,您必须使用依赖属性。下面是一个可行的示例:

页面

<Page x:Class="JonSkeetTest.SkeetPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:JonSkeetTest="clr-namespace:JonSkeetTest" mc:Ignorable="d" 
      JonSkeetTest:SkeetPage.MyProperty="Testing.."
      d:DesignHeight="300" d:DesignWidth="300"
    Title="SkeetPage">
   
    <Grid>
        <Button Click="ButtonTest_Pressed"></Button>
    </Grid>
</Page>

后台代码

using System.Windows;
using System.Windows.Controls;

namespace JonSkeetTest
{
    public partial class SkeetPage
    {
        public SkeetPage()
        {
            InitializeComponent();
        }

        public static readonly DependencyProperty MyPropertyProperty = DependencyProperty.Register(
          "MyProperty",
          typeof(string),
          typeof(Page),
          new FrameworkPropertyMetadata(null,
              FrameworkPropertyMetadataOptions.AffectsRender
          )
        );

        public static void SetMyProperty(UIElement element, string value)
        {
            element.SetValue(MyPropertyProperty, value);
        }
        public static string GetMyProperty(UIElement element)
        {
            return element.GetValue(MyPropertyProperty).ToString();
        }

        public string MyProperty
        {
            get { return GetValue(MyPropertyProperty).ToString(); }
            set { SetValue(MyPropertyProperty, value); }
        }

        private void ButtonTest_Pressed(object sender, RoutedEventArgs e)
        {
            MessageBox.Show(MyProperty);
        }
    }
}

如果您按下按钮,将在一个消息框中看到“Testing...”。

3
你可以将<Page>元素声明为<TestPage>元素:
<YourApp:TestPage 
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
  xmlns:YourApp="clr-namespace:YourApp"
  MyProperty="Hello">
</YourApp:TestPage>

那么这样做就可以解决问题,但您会失去InitializeComponent()和标准的设计工具。但设计模式似乎仍然可以完美地工作,尽管我没有进行全面测试。

更新:这可以编译和运行,但实际上并没有设置MyProperty。您还将失去在XAML中绑定事件处理程序的能力(虽然可能有一种方法可以恢复,但我不知道)。

更新2:来自@Fredrik Mörk的工作示例,它可以设置属性,但不支持在XAML中绑定事件处理程序:

后台代码:

namespace WpfApplication1
{
    public partial class MainWindow : Window
    {
        protected override void OnActivated(EventArgs e)
        {
            this.Title = MyProperty;
        }      

        public string MyProperty { get; set; }
    }
}

XAML:

<WpfApplication1:MainWindow
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:WpfApplication1="clr-namespace:WpfApplication1" 
    Title="MainWindow" 
    Height="350" 
    Width="525"
    MyProperty="My Property Value"> 
</WpfApplication1:MainWindow>

抱歉,稍有失误。您需要使用xmlns:YourApp="clr-namespace:YourApp"并声明为<YourApp:TestPage>。已更新答案。 - Håvard S
在工作时,这个解决方案还会移除在XAML中绑定事件处理程序的可能性,这可能会带来一些不便。 - Fredrik Mörk
1
@Håvard:你并不完全错。它确实可以工作,但是有一些缺点。 - Fredrik Mörk
@Fredrik Mörk 我的属性没有设置,所以不行。如果你的代码可以工作,请分享给我们,让我们都受益。 :) - Håvard S
1
XAML:尽可能删除所有面向对象的概念 - Chris S
显示剩余6条评论

2
你的XAML等同于以下内容:
<Page x:Class="SkeetProblem.TestPage"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Page.MyProperty>MyPropertyValue</Page.MyProperty> 
</Page>

这显然是非法的。XAML 文件由 Application 类的静态 LoadComponent 方法加载,参考文献 中写道:

加载位于指定统一资源标识符(URI)位置的 XAML 文件,并将其转换为由 XAML 文件的根元素指定的对象实例。

这意味着您只能为根元素指定的类型设置属性。因此,您需要对 Page 进行子类化,并将该子类指定为您的 XAML 的根元素。

2
这对我很有帮助。
<Window x:Class="WpfSandbox.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfSandbox"        
    xmlns:src="clr-namespace:WpfSandbox" 
    Title="MainWindow" Height="350" Width="525"
    src:MainWindow.SuperClick="SuperClickEventHandler">
</Window>

因此,这可能适用于原始问题(未尝试)。请注意xmlns:src。

<Page x:Class="WpfSandbox.TestPage"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:local="clr-namespace:WpfSandbox"        
  xmlns:src="clr-namespace:WpfSandbox" 
  src:TestPage.MyProperty="MyPropertyValue">
</Page>

1

我刚试图用一些不同的意图做同样的事情。

实际上,真正的答案是:你需要正确完成Set方法的WPF约定。 如此处所述:http://msdn.microsoft.com/en-us/library/ms749011.aspx#custom 如果您要定义名为Xxx的附加属性,则必须定义SetXxx和GetXxx方法。

因此,请参阅此工作示例:

public class Lokalisierer : DependencyObject
{
    public Lokalisierer()
    {
    }

    public static readonly DependencyProperty LIdProperty = 
        DependencyProperty.RegisterAttached("LId", 
                                            typeof(string), 
                                            typeof(Lokalisierer),
                                            new FrameworkPropertyMetadata( 
                                                  null,
                                                     FrameworkPropertyMetadataOptions.AffectsRender | 
                                                     FrameworkPropertyMetadataOptions.AffectsMeasure,
                                                     new PropertyChangedCallback(OnLocIdChanged)));

    private static void OnLocIdChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
    // on startup youll be called here
    }

    public static void SetLId(UIElement element, string value)
    {
      element.SetValue(LIdProperty, value);
    }
    public static string GetLId(UIElement element)
    {
      return (string)element.GetValue(LIdProperty);
    }


    public string LId
    {
        get{    return (string)GetValue(LIdProperty);   }
        set{ SetValue(LIdProperty, value); }
    }
}

还有WPF部分:

<Window x:Class="LokalisierungMitAP.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:me="clr-namespace:LokalisierungMitAP"
Title="LokalisierungMitAP" Height="300" Width="300"
>
<StackPanel>
    <Label  me:Lokalisierer.LId="hhh">Label1</Label>
   </StackPanel>

顺便提一下:你还需要继承 DependencyObject


1

我的建议是使用带有默认值的DependencyProperty

    public int MyProperty
    {
        get { return (int)GetValue(MyPropertyProperty); }
        set { SetValue(MyPropertyProperty, value); }
    }

    public static readonly DependencyProperty MyPropertyProperty =
        DependencyProperty.Register("MyProperty", typeof(int), typeof(MyClass), 
               new PropertyMetadata(1337)); //<-- Default goes here

将控件的属性视为您向外部世界公开使用的内容。

如果您希望使用自己的属性,则可以使用ElementNameRelativeSource绑定。

关于过度设计的问题,DependencyPropertiesDependencyObjects密不可分;)

无需进一步的XAML,PropertyMetadata中的默认值就足够了。

如果您真的希望将其放入XAML中,请选择基类解决方案,或者神明禁止,引入可附加属性,该属性也可以用于任何其他控件。


为什么这个不起作用?不需要更多的XAML...默认值会完成其余部分... - Arcturus

1

回答涉及Silverlight。

没有简单明显的方法以你想要的方式使用普通属性,这将需要在途中做出一些妥协。

实际上并不起作用:

有人建议使用依赖属性。那行不通,从Xaml的角度来看,它仍然是一个公共属性。附加属性可以工作,但这会使代码处理变得丑陋。

接近但无效:

XAML和类可以完全分离,就像这样:

<local:PageWithProperty
           xmlns:local="clr-namespace:StackoverflowSpikes"
           xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
           xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
           xmlns:navigation="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation"
    Message="Hello World"
    Loaded="PageWithProperty_Loaded"
    Title="Some Title"
           >
    <Grid x:Name="LayoutRoot">
        <TextBlock Text="{Binding Parent.Message, ElementName=LayoutRoot}" />
    </Grid>
</local:PageWithProperty>

代码:-

public class PageWithProperty : Page
{

        internal System.Windows.Controls.Grid LayoutRoot;

        private bool _contentLoaded;

        public void InitializeComponent()
        {
            if (_contentLoaded) {
                return;
            }
            _contentLoaded = true;
            System.Windows.Application.LoadComponent(this, new System.Uri("/StackoverflowSpikes;component/PageWithProperty.xaml", System.UriKind.Relative));
            this.LayoutRoot = ((System.Windows.Controls.Grid)(this.FindName("LayoutRoot")));
         }

    public PageWithProperty()
    {
        InitializeComponent();
    }

    void PageWithProperty_Loaded(object sender, RoutedEventArgs e)
    {
        MessageBox.Show("Hi");
    }
    public string Message {get; set; }

}

然而,您会失去一些来自设计师的支持。特别是,您将不得不创建字段来保存对命名元素的引用,并在自己实现的InitialiseComponent中分配它们(在我看来,所有这些自动字段用于命名项目并不一定是件好事)。此外,设计师不会为您动态创建事件代码(尽管奇怪的是,它似乎知道如何导航到您手动创建的事件),但在Xaml中定义的事件将在运行时连接。

在我看来最佳选择:

最好的折衷方案已经由abhishek发布,使用一个shim基类来保存属性。最小的努力,最大的兼容性。


0

你可以使用样式来设置属性:

<Page.Style>
    <Style TargetType="{x:Type wpfSandbox:TestPage}">
        <Setter Property="MyProperty" Value="This works" />
    </Style>
</Page.Style>

但它只适用于依赖属性!

public static readonly DependencyProperty MyPropertyProperty = DependencyProperty.Register(
    nameof(MyProperty), typeof(string), typeof(Page),
    new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender));

public string MyProperty
{
    get { return (string)GetValue(MyPropertyProperty); }
    set { SetValue(MyPropertyProperty, value); }
}

0

你需要将它定义为可附加属性,才能像这样访问它。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接