How to create a RangeAttribute for WindowsForms?

匿名 (未验证) 提交于 2019-12-03 03:04:01

问题:

I would like to create a metadata attribute called RangeAttribute without external tools like PostSharp as seen in this answer because it requires the paid version of the library.

The only official information that I've found about this is this MSDN documentation, but absurdly that page only explains how to declare the class and the inheritance... NOTHING more, so I'm more than lost.

My intention is to transform this code:

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 

Into something reusable and simplified, like this:

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 

So to accomplish this task I've started writting the attribute, but is uncomplete due to an insufficient documentation or examples then I don't know how to procceed to evaluate the values in the setter of a property without adding manually the getter/setter in the property of the code above:

<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 

The Attribute code above will ignore the values that are not in range, i understand this is because I'm not evaluating nothing, but I don't know how to do it.

回答1:

Well there are other AOP frameworks/libraries available for .Net platform, Spring.net AOP, KingAOP, FluentAOP, Afterthought,... to name a few.

Here is a proposed solution using Afterthought.

NOTE: We can divide AOP frameworks to two major categories based on techniques used for interception, frameworks which inject the interception code during Compile-time (Compile-time IL weaving) and the ones which do the injection during run-time (run-time IL Weaving or Dynamic IL-weaving). PostSharp supports both methods in current version, each technique has its own pros and cons which is out of scope of this answer, for more information you can refer to http://www.postsharp.net/aop.net

In this sample we chose Compile-time IL-Weaving based on Afterthought framework (Afterthought only supports compile-time IL weaving)

1-Prepration

you can get Afterthought from https://github.com/r1pper/Afterthought/releases (you can download the binaries or you can get the source and compile it by yourself, I go the binary route here)

extract the package there are 2 files Afterthought.dll and Afterthought.Amender.exe, reference to afterthought.dll.

As I said before Afterthought uses compile-time IL weaving and this is exactly what Afterthought.Amender.exe does.

we should call Amender after each build to inject the interception code to our assembly:

Afterthought.Amender.exe "assembly"

we can automate the task by defining a new Post Build event for our project (this is exactly what PostSharp does) Here I copied Afterthought folder in my project's directory and this is my post build event(you may need to change the post event based on your folder location):

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

OK, now we are ready to write our code

2- Sample Code with Range control for Integer numbers between [0,10]

In this sample we define a range control attribute and name it RangeAttribute an try to intercept properties setter method to check if our set value is within the range.

Interception Code and Injection:

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 

Classes and Definitions:

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 

Sample:

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 

now when you build your project you should see similar message in your build output:

Injecting range check here!

Amending AopVb3.exe (3.685 seconds)

========== Rebuild All: 1 succeeded, 0 failed, 0 skipped ==========

and the output of the console should be :

try setting value to 5

5

try setting value to 20

property 'Value' value should be between [0,10]



回答2:

New, revised, updated answer; also deleted my comments below:

Ok, here is something which can work and is not horribly intrusive. First, a few words about what you have been looking at.

Attributes provide meta data for a Type or Property etc. Since it is compiled into the final assembly, the only way to get at them is via Reflection. You cant just add an Attribute to something and have it magically do something without a some code somewhere to activate the methods etc in it. The attribute itself then has to use Reflection to determine which Type and property it is associated with. In some cases, there is an entire library to support these activities.

The NET Range you looked at a few days back is much more involved than it appears. Note that there is no Validate or CheckValue type method, just a Boolean IsValid! Aside from being a Web-Thing, it also appears to be related to databinding - there is also a RangeAttributeAdapter, a ValidationArttribute (Range inherits from this) and ValidationContext. The RangeAttribute is simply where the values are specified and there is much, much more going on than just a simple attribute.

Other things, like PostSharp are "Weavers" - simple enough to use (kind of), but they rewrite your code to inject what amounts to wrappers to monitor property changes and call your range validation method(s). Then more reflection to post the validated data back to the property.

The point: none of the things you have looked at are just Attributes, there is much more going on.

The following is a RangeManager which isn't as transparent as a Weaver but it is simpler. At the heart is a Range Attribute where you can specify the valid min/max. But there is also a RangeManager object you need to create which will do the heavy lifting of finding the property, converting from setter method, finding the valid range and then testing it. It scours the type it is instanced in to find all the relevant properties.

It is frugal on the Reflection calls. When instanced, the Manager finds all the tagged properties and saves a reference to the RangeAttribute instance so that every time you set a property there are not several new Reflection methods invoked.

If nothing else it shows a little of what is involved.

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 

As noted in the code comments, you could inherit RangerAttribute so you could provide more extensive range tests.

Sample usage:

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 

Test Code:

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) 

There you have it: 220 lines of code for an Attribute based range validator to replace the following in your setters:

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

To me, the only thing which gets it past my gag factor as far as offloading data validation to something outside the property and class is that the ranges used are listed right there above the property.


Attributes know nothing about the Properties they decorate. It is up to Something Else to make that connection, and that Something Else will need to use Reflection to get at the Attribute data.

Likewise, Properties know nothing about the Attributes assigned to them because the Attributes are metadata intended for the compiler or Something Else like a serializer. This Something Else must also use Reflection to make a connection between the two tiers (Type methods and meta data).

In the end, Something Else ends up being either a tool to rewrite your emitted assembly in order to provide the range checking service, or a library to provide the service via a method as shown above.

The hurdle to something being more transparent is that there is not something like a PropertyChanged event to hook onto (see PropertyInfo).

  • Implement IComparable for use in RangeCheck


标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!