【问题标题】:How to create a RangeAttribute for WindowsForms?如何为 Windows 窗体创建范围属性?
【发布时间】:2014-11-26 16:58:20
【问题描述】:

我想创建一个名为 RangeAttribute 的元数据属性,而不需要像 this answer 中所见的 PostSharp 这样的外部工具,因为它需要库的付费版本。

我发现的唯一官方信息是this MSDN documentation,但荒谬的是,该页面只解释了如何声明类和继承......没有更多,所以我不仅仅是迷路了。

我的意图是转换这段代码:

Public NotInheritable Class MyType

''' <summary>
''' Gets or sets the value.
''' </summary>
''' <value>The value.</value>
Public Property MyProperty As Integer
    Get
        Return Me._MyValue
    End Get
    Set(ByVal value As Integer)

        If value < Me._MyValueMin Then
            If Me._MyValueThrowRangeException Then
                Throw New ArgumentOutOfRangeException("MyValue", Me._MyValueExceptionMessage)
            End If
            Me._MyValue = Me._MyValueMin

        ElseIf value > Me._MyValueMax Then
            If Me._MyValueThrowRangeException Then
                Throw New ArgumentOutOfRangeException("MyValue", Me._MyValueExceptionMessage)
            End If
            Me._MyValue = Me._MyValueMax

        Else
            Me._MyValue = value

        End If

    End Set
End Property
Private _MyValue As Integer = 0I
Private _MyValueMin As Integer = 0I
Private _MyValueMax As Integer = 10I
Private _MyValueThrowRangeException As Boolean = True
Private _MyValueExceptionMessage As String = String.Format("The valid range is beetwen {0} and {1}",
                                                           Me._MyValueMin, Me._MyValueMax)

End Class

变成可重用和简化的东西,像这样:

Public NotInheritable Class MyType

    ''' <summary>
    ''' Gets or sets the value.
    ''' Valid range is between 0 and 10.
    ''' </summary>
    ''' <value>The value.</value>
    <RangeAttribute(0, 10, ThrowRangeException:=False, ExceptionMessage:="")>
    Public Property MyProperty As Integer

End Class

因此,为了完成这项任务,我已经开始编写属性,但由于文档或示例不足而未完成,然后我不知道如何在不手动添加的情况下评估属性的 setter 中的值上面代码属性中的getter/setter:

<AttributeUsage(AttributeTargets.Property Or
                AttributeTargets.Parameter Or
                AttributeTargets.ReturnValue Or
                AttributeTargets.Field, 
                AllowMultiple:=False)>
Public Class RangeAttribute : Inherits Attribute

    ''' <summary>
    ''' Indicates the Minimum range value.
    ''' </summary>
    Public Minimum As Single

    ''' <summary>
    ''' Indicates the Maximum range value.
    ''' </summary>
    Public Maximum As Single

    ''' <summary>
    ''' Determines whether to throw an exception when the value is not in range.
    ''' </summary>
    Public ThrowRangeException As Boolean

    ''' <summary>
    ''' Indicates the exception message to show when the value is not in range.
    ''' </summary>
    Public ExceptionMessage As String

    ''' <summary>
    ''' Initializes a new instance of the <see cref="RangeAttribute"/> class.
    ''' </summary>
    ''' <param name="Minimum">The minimum range value.</param>
    ''' <param name="Maximum">The maximum range value.</param>
    Public Sub New(ByVal Minimum As Single,
                   ByVal Maximum As Single)

        Me.New(Minimum, Maximum, ThrowRangeException:=False, ExceptionMessage:=String.Empty)

    End Sub

    ''' <summary>
    ''' Initializes a new instance of the <see cref="RangeAttribute"/> class.
    ''' </summary>
    ''' <param name="Minimum">The minimum range value.</param>
    ''' <param name="Maximum">The maximum range value.</param>
    ''' <param name="ThrowRangeException">
    ''' Determines whether to throw an exception when the value is not in range.
    ''' </param>
    Public Sub New(ByVal Minimum As Single,
                   ByVal Maximum As Single,
                   ByVal ThrowRangeException As Boolean,
                   Optional ByVal ExceptionMessage As String = "")

        Me.Minimum = Minimum
        Me.Maximum = Maximum
        Me.ThrowRangeException = ThrowRangeException

        If Not String.IsNullOrEmpty(ExceptionMessage) Then
            Me.ExceptionMessage = ExceptionMessage
        Else
            Me.ExceptionMessage = String.Format("The valid range is beetwen {0} and {1}", Minimum, Maximum)
        End If

    End Sub

End Class

上面的属性代码将忽略不在范围内的值,我理解这是因为我没有评估任何内容,但我不知道该怎么做。

【问题讨论】:

  • Fody 做了类似的事情。它在编译时注入 INotifyPropertyChanged 代码。随意查看源代码,看看是否对您有帮助。

标签: .net vb.net winforms types attributes


【解决方案1】:

还有其他可用于 .Net 平台的 AOP 框架/库,Spring.net AOP、KingAOP、FluentAOP、Afterthought,等等。

这是使用Afterthought 的建议解决方案。

注意:我们可以根据用于拦截的技术将 AOP 框架分为两大类,在编译时(编译时 IL 编织)期间注入拦截代码的框架和在在运行时注入(运行时 IL Weaving 或 Dynamic IL-weaving)。 PostSharp 在当前版本中支持这两种方法,每种技术都有其优缺点,超出了本答案的范围,更多信息可以参考http://www.postsharp.net/aop.net

在这个示例中,我们选择了基于Afterthought框架的编译时IL-Weaving(Afterthought仅支持编译时IL编织)

1-准备

你可以从https://github.com/r1pper/Afterthought/releases获得Afterthought(你可以下载二进制文件或者你可以获取源代码并自己编译,我在这里走二进制路线)

解压包有Afterthought.dll和Afterthought.Amender.exe两个文件,参考afterthought.dll。

正如我之前所说,Afterthought 使用编译时 IL 编织,这正是 Afterthought.Amender.exe 所做的。

我们应该在每次构建后调用 Amender 将拦截代码注入我们的程序集:

Afterthought.Amender.exe“程序集”

我们可以通过为我们的项目定义一个新的Post Build event 来自动执行任务(这正是 PostSharp 所做的)这里我将 Afterthought 文件夹复制到我的项目目录中,这是我的构建后事件(您可能需要根据您的文件夹位置更改发布事件):

"$(ProjectDir)Afterthought\Afterthought.Amender.exe" "$(TargetPath)"

好的,现在我们可以编写代码了

2- [0,10] 之间整数的范围控制示例代码

在这个示例中,我们定义了一个范围控制属性并将其命名为RangeAttribute 一个尝试拦截属性设置器方法以检查我们的设置值是否在范围内。

拦截代码和注入:

Imports Afterthought
Imports System.Reflection

Public Class RangeAmendment(Of T)
    Inherits Amendment(Of T, T)
    Public Sub New()
        MyBase.New()
        Console.WriteLine("Injecting range check here!")

        Properties.AfterSet(Sub(instance As T, pName As String, pvOld As Object, pv As Object, pvNew As Object)

                                Dim p As PropertyInfo = instance.GetType().GetProperty(pName)
                                Dim att As RangeAttribute = p.GetCustomAttribute(Of RangeAttribute)()
                                If att Is Nothing Then Return

                                Dim v As Object = p.GetValue(instance)
                                Dim castedValue As Integer = Convert.ToInt32(v)
                                If (castedValue < att.Min OrElse castedValue > att.Max) Then
                                    Throw New RangeException(p.Name, att.Min, att.Max)
                                End If

                            End Sub)
    End Sub
End Class

类和定义:

Public Class RangeAttribute
    Inherits Attribute

    Public Property Max As Integer

    Public Property Min As Integer

    Public Sub New(ByVal min As Integer, ByVal max As Integer)
        MyBase.New()
        Me.Min = min
        Me.Max = max
    End Sub
End Class

Public Class RangeException
    Inherits ApplicationException
    Public Sub New(ByVal propertyName As String, ByVal min As Integer, ByVal max As Integer)
        MyBase.New(String.Format("property '{0}' value should be between [{1},{2}]", propertyName, min, max))
    End Sub
End Class



<Amendment(GetType(RangeAmendment(Of )))>
Public Class TestClass
    <Range(0, 10)>
    Public Property Value As Integer

    Public Sub New()
        MyBase.New()
    End Sub
End Class

示例:

Module Module1

        Sub Main()
            Dim test = New TestClass()
    
            Try
                Console.WriteLine("try setting value to 5")
                test.Value = 5
                Console.WriteLine(test.Value)
    
                Console.WriteLine("try setting value to 20")
                test.Value = 20
                Console.WriteLine(test.Value)
    
            Catch ex As RangeException
                Console.WriteLine(ex.Message)
            End Try
    
            Console.ReadKey()
        End Sub
    
    End Module

现在,当您构建项目时,您应该会在构建输出中看到类似的消息:

注射范围检查这里!

修改 AopVb3.exe(3.685 秒)

==========全部重建:1成功,0失败,0跳过==========

控制台的输出应该是:

尝试将值设置为 5

5

尝试将值设置为 20

属性“值”值应在 [0,10] 之间

【讨论】:

    【解决方案2】:

    新的、修订的、更新的答案;在下面还删除了我的 cmets:

    好的,这里有一些可以工作并且不会造成严重干扰的东西。首先,谈谈你一直在看的东西。

    属性为类型或属性等提供元数据。由于它被编译到最终程序集中,因此获取它们的唯一方法是通过反射。你不能只是给某事添加一个属性,然后让它神奇地做某事,而无需在某处使用一些代码来激活其中的方法等。然后属性本身必须使用反射来确定它与哪个类型和属性相关联。在某些情况下,有一个完整的库来支持这些活动。

    您几天前看到的 NET Range 比看起来要复杂得多。请注意,没有Validate 或CheckValue 类型方法,只是一个布尔值IsValid!除了作为 Web-Thing 之外,它似乎也与数据绑定有关 - 还有一个 RangeAttributeAdapter、一个 ValidationArttribute(范围继承自此)和 ValidationContext。 RangeAttribute 只是指定值的地方,而且除了一个简单的属性之外,还有很多很多事情要做。

    其他东西,比如 PostSharp 是“Weavers”——使用起来足够简单(有点),但它们重写您的代码以注入相当于包装器的内容以监控属性更改并调用您的范围验证方法(s)。然后进行更多反射以将经过验证的数据发布回属性。

    重点:你所看到的都不是只是属性,还有很多事情要做。

    下面是RangeManager,它不像 Weaver 那样透明,但更简单。核心是一个范围属性,您可以在其中指定有效的最小值/最大值。但是您还需要创建一个RangeManager 对象,它将完成查找属性、从setter 方法转换、查找有效范围然后对其进行测试的繁重工作。它会搜索实例化的类型以查找所有相关属性。

    反射调用很节俭。实例化时,Manager 会查找所有标记的属性并保存对 RangeAttribute 实例的引用,这样每次设置属性时都不会调用多个新的反射方法。

    如果没有其他内容,它会显示一些所涉及的内容。

    Imports System.Reflection
    Imports System.Globalization
    
    Public Class RangeManager
    
        <AttributeUsage(AttributeTargets.Property)>
        Public Class RangerAttribute
            Inherits Attribute
    
            Public Property Minimum As Object
            Public Property Maximum As Object
            Private min As IComparable
            Private max As IComparable
    
            ' converter: used by IsValid which is not overloaded
            Private Property Conversion() As Func(Of Object, Object)
    
            Public Property VarType As Type
    
            Public Sub New(n As Integer, x As Integer)
                Minimum = n
                Maximum = x
                VarType = GetType(Integer)
                min = CType(Minimum, IComparable)
                max = CType(Maximum, IComparable)
                Conversion = Function(v) Convert.ToInt32(v, 
                             CultureInfo.InvariantCulture)
            End Sub
    
            Public Sub New(n As Single, x As Single)
                Minimum = n
                Maximum = x
                VarType = GetType(Single)
                min = CType(Minimum, IComparable)
                max = CType(Maximum, IComparable)
                Conversion = Function(v) Convert.ToSingle(v,
                             CultureInfo.InvariantCulture)
            End Sub
    
            Public Sub New(n As Double, x As Double)
                Minimum = n
                Maximum = x
                VarType = GetType(Double)
                min = CType(Minimum, IComparable)
                max = CType(Maximum, IComparable)
                Conversion = Function(v) Convert.ToDouble(v, 
                             CultureInfo.InvariantCulture)
            End Sub
    
            ' overridable so you can inherit and provide more complex tests
            ' e.g. String version might enforce Casing or Length
            Public Overridable Function RangeCheck(value As Integer) As Integer
                If min.CompareTo(value) < 0 Then Return CInt(Minimum)
                If max.CompareTo(value) > 0 Then Return CInt(Maximum)
                Return value
            End Function
    
            Public Overridable Function RangeCheck(value As Single) As Single
                If min.CompareTo(value) < 0 Then Return CSng(Minimum)
                If max.CompareTo(value) > 0 Then Return CSng(Maximum)
                Return value
            End Function
    
            Public Overridable Function RangeCheck(value As Double) As Double
                If min.CompareTo(value) < 0 Then Return CDbl(Minimum)
                If max.CompareTo(value) > 0 Then Return CDbl(Maximum)
                Return value
            End Function
    
            ' rather than throw exceptions, provide an IsValid method
            ' lifted from MS Ref Src
            Public Function IsValid(value As Object) As Boolean
                ' dont know the type
                Dim converted As Object
    
                Try
                    converted = Me.Conversion(value)
                Catch ex As InvalidCastException
                    Return False
                Catch ex As NotSupportedException
                    Return False
    
                    ' ToDo: add more Catches as you encounter and identify them
                End Try
    
                Dim min As IComparable = CType(Minimum, IComparable)
                Dim max As IComparable = CType(Maximum, IComparable)
    
                Return min.CompareTo(converted) <= 0 AndAlso 
                              max.CompareTo(converted) >= 0
            End Function
    
        End Class
    
        ' map of prop names to setter method names
        Private Class PropMap
            Public Property Name As String      ' not critical - debug aide
            Public Property Setter As String
    
            ' store attribute instance to minimize reflection
            Public Property Range As RangerAttribute
    
            Public Sub New(pName As String, pSet As String, r As RangerAttribute)
                Name = pName
                Setter = pSet
                Range = r
            End Sub
        End Class
    
    
        Private myType As Type             ' not as useful as I'd hoped
        Private pList As List(Of PropMap)
    
        Public Sub New()
            ' capture calling Type so it does not need to be specified
            Dim frame As New StackFrame(1)
            myType = frame.GetMethod.DeclaringType
    
            ' create a list of Props and their setter names
            pList = New List(Of PropMap)
    
            BuildPropMap()
        End Sub
    
        Private Sub BuildPropMap()
            ' when called from a prop setter, StackFrame reports
            ' the setter name, so map these to the prop name
    
            Dim pi() As PropertyInfo = myType.GetProperties
    
            For Each p As PropertyInfo In pi
                ' see if this prop has our attr
                Dim attr() As RangerAttribute =
                    DirectCast(p.GetCustomAttributes(GetType(RangerAttribute), True), 
                                        RangerAttribute())
    
                If attr.Count > 0 Then
                    ' find it
                    For n As Integer = 0 To attr.Count - 1
                        If attr(n).GetType = GetType(RangerAttribute) Then
                            pList.Add(New PropMap(p.Name, p.GetSetMethod.Name, attr(n)))
                            Exit For
                        End If
                    Next
                End If
    
            Next
    
        End Sub
    
        ' can be invoked only from Setter!
        Public Function IsValid(value As Object) As Boolean
            Dim frame As New StackFrame(1)
            Dim pm As PropMap = GetPropMapItem(frame.GetMethod.Name)
            Return pm.Range.IsValid(value)
        End Function
    
        ' validate and force value to a range
        Public Function CheckValue(value As Integer) As Integer
            Dim frame As New StackFrame(1)
            Dim pm As PropMap = GetPropMapItem(frame.GetMethod.Name)
    
            If pm IsNot Nothing Then
                Return pm.Range.CheckValue(value)
            Else
                Return value      ' or something else
            End If
    
        End Function
    
        ' other types omitted for brevity:   
        Public Function CheckValue(value As Double) As Double
           ...
        End Function
    
        Public Function CheckValue(value As Single) As Single
           ...
        End Function
    
        Private Function GetPropMapItem(setterName As String) As PropMap
            For Each p As PropMap In pList
                If p.Setter = setterName Then
                    Return p
                End If
            Next
            Return Nothing
        End Function
    
    End Class
    

    如代码 cmets 中所述,您可以继承 RangerAttribute,以便提供更广泛的范围测试。

    示例用法:

    Imports RangeManager
    
    Public Class FooBar
    
        Public Property Name As String
    
        Private _IntVal As Integer
        <Ranger(1, 10)>
        Public Property IntValue As Integer
            Get
                Return _IntVal
            End Get
            Set(value As Integer)
                _IntVal = rm.CheckValue(value)
            End Set
        End Property  
    
        ' this is a valid place to use Literal type characters
        ' to make sure the correct Type is identified
        Private _sngVal As Single
        <Ranger(3.01F, 4.51F)>
        Public Property SngValue As Single
            Get
                Return _sngVal
            End Get
            Set(value As Single)
                If rm.IsValid(value) = False Then
                    Console.Beep()
                End If
                _sngVal = rm.CheckValue(value)
            End Set
        End Property
    
        Private rm As RangeManager
    
        Public Sub New(sName As String, nVal As Integer, dVal As Decimal)
            ' rm is mainly used where you want to validate values
            rm = New RangeManager
    
            ' test if this can be used in the ctor
            Name = sName
            IntValue = nVal * 100
            DblValue = dVal
    
        End Sub
    
    End Class
    

    测试代码:

    Dim f As New FooBar("ziggy", 1, 3.14)
    
    f.IntValue = 900
    Console.WriteLine("val tried: {0} result: {1}", 900.ToString, f.IntValue.ToString)
    
    f.IntValue = -23
    Console.WriteLine("val tried: {0} result: {1}", (-23).ToString, f.IntValue.ToString)
    
    
    f.SngValue = 98.6
    Console.WriteLine("val tried: {0} result: {1}", (98.6).ToString, f.SngValue.ToString)
    

    你有它:基于属性的范围验证器的 220 行代码来替换你的 setter 中的以下内容:

    If value < Minimum Then value = Minimum
    If value > Maximum Then value = Maximum
    

    对我而言,将数据验证卸载到属性 and 类之外的唯一因素是,使用的范围列在属性上方。


    Attributes 对他们装饰的属性一无所知。 Something Else 可以建立这种连接,而 Something Else 将需要使用反射来获取 Attribute 数据。

    同样,Properties 对分配给它们的Attributes 一无所知,因为Attributes 是用于编译器或其他东西之类的序列化程序的元数据。这个Something Else还必须使用反射在两个层(类型方法和元数据)之间建立连接。

    最后,Something Else 最终要么成为重写发出的程序集以提供范围检查服务的工具,要么成为通过上述方法提供服务的库。

    更透明的障碍是没有像 PropertyChanged 事件这样可以挂钩(请参阅PropertyInfo)。

    • 实现 IComparable 以在 RangeCheck 中使用

    【讨论】:

    • I dont think you want to be throwing exceptions when the value is out of range 为什么不呢?在调试或发现其中一个范围的失败可能非常有用,ASP.Net 的RangeAttribute 具有Throwexception 功能(我不记得确切的参数名称),可以选择设置带有可选的异常消息。
    • 关于代码,首先谢谢你,但这正是我想避免的,我的目的是简化属性以避免编写 getter/setter 然后附加 range 属性。如果您注意到我显示的第一个代码并且您的最后一个代码基本相同,我的没有反射,而你的有反射,我真的看不到反射方法有任何好处,您正在评估 setter 中的值,是同样的东西,但以更重的方式制造。谢谢你的回答
    • 请使用PostSharp:stackoverflow.com/questions/20019130/… 查看此问题中的解决方案,因为您看到属性中的唯一要求是“附加”范围属性,没有getter 和setter,我没有对LocationInterceptionAspect 到底是做什么的有任何想法,或者我该如何为此做类似的事情,但这意味着属性yes can do something,然后一旦发现如何只在顶部添加范围属性就不需要getter/setter的属性(以及范围属性的类)。谢谢
    • 我知道PostSharp是一群专业的开发人员写的,代码行数上千,但我不假装模仿整个库的功能,我只是提一下给出一个真实示例的特定功能表明实现RangeAttribute 是不必要的,仍然手动评估写入getter/setter的值。
    • 另一方面,我从没想过反射应该是实现这一点所必需的,每次设置属性时加载程序集似乎不是最好的性能,但无论如何如果它需要实现一个 RangeAttibute 然后我不太关心它的用法,我想做的只是在属性声明上添加我的自定义属性,然后从 Range 相关类中管理所有必要的东西,比如评估和任何需要的东西,但不是全部在属性 getter/setter 中并合并更多代码,你能帮我实现类似的东西吗?
    猜你喜欢
    • 2021-10-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-10-30
    • 1970-01-01
    • 2010-10-20
    • 2015-08-20
    • 2018-04-24
    相关资源
    最近更新 更多