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