2014-10-01 28 views
1

我想創建一個名爲RangeAttribute的元數據屬性,而不需要像PostSharp這樣的外部工具,如this answer所示,因爲它需要庫的付費版本。如何爲WindowsForms創建RangeAttribute?

我發現的唯一官方信息是this MSDN documentation,但荒謬的是,該頁面只解釋瞭如何聲明類和繼承...沒有更多的東西,所以我比輸了更多。

我的目的是要改造這個代碼:

到的東西可重用和簡化的,就像這樣:

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 

上面的屬性代碼將忽略不在範圍內的值,我明白這是因爲我沒有評估任何東西,但我不知道該怎麼做。

+0

[Fody(https://github.com/Fody/PropertyChanged)做這樣的事情類似之間。它在編譯時注入INotifyPropertyChanged代碼。隨時查看來源,看看是否可以幫助你。 – 2014-10-01 19:33:43

回答

3

那麼還有其他的AOP框架/庫可供.Net平臺,Spring.net AOPKingAOPFluentAOPAfterthought,...僅舉幾例。

下面是使用Afterthought的建議解決方案。

注:我們可以把AOP框架,基於用於截取等技術兩大類,其中在編譯時(編譯時IL織)注入攔截代碼框架和運行過程中做注射的那些(運行時間IL編織或動態IL編織)。 PostSharp支持當前版本中的兩種方法,每種技術都有其優點和缺點,不在本答案的範圍內,因爲您可以參考更多信息http://www.postsharp.net/aop.net

在本示例中,我們選擇了編譯時IL-織造Afterthought框架(Afterthought只支持編譯時IL織)

1 Prepration

你可以從https://github.com/r1pper/Afterthought/releases有感得到(你可以下載的二進制文件,或者你可以得到源,並通過自己編譯它,我去這裏的二進制路線)

提取包中有2個文件Afterthought.dllAfterthought.Amender.exe,提及afterthought.dll

正如我之前所說的Afterthought使用編譯時IL編織,這正是Afterthought.Amender.exe所做的。

我們應該每個版本後打電話修正器注入攔截代碼,我們的裝配:

Afterthought.Amender.exe「集結號」

我們可以通過定義一個新的Post Build event自動化任務對於我們的項目(這正是PostSharp做的)在這裏,我複製了Afterthought文件夾在我的項目目錄中,這是我的發佈後事件(您可能需要根據您的文件夾位置更改發佈事件):

「$(PROJECTDIR)有感\ Afterthought.Amender.exe」 「$(TARGETPATH)」

OK,現在我們已經準備好寫我們的代碼

2-示例代碼與範圍控制整數[0,10]

在本示例中,我們定義一個範圍控制屬性並將其命名爲RangeAttribute嘗試攔截屬性setter方法來檢查我們的設置值是否在範圍內。

攔截代碼和注:

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

嘗試設置值20

財產 '值' 值應爲[0,10]

1

新的,修訂的,更新的答案;也刪除了我的意見如下:

好吧,這是可以工作的東西,並沒有可怕的侵入性。首先,談談你一直在看什麼。

屬性爲類型或屬性等提供元數據。由於它被編譯到最終的程序集中,唯一的方法是通過反射。你不能只是添加一個屬性到某個東西,讓它神奇地做一些沒有一些代碼的地方來激活它的方法等。該屬性本身必須使用反射來確定它與哪個類型和屬性相關聯。在某些情況下,有一個完整的圖書館來支持這些活動。

你在幾天前看過的NET Range比看起來要複雜得多。請注意,沒有ValidateCheckValue類型的方法,只是布爾型IsValid!除了是一個網絡事物,它也似乎與數據綁定有關 - 還有一個RangeAttributeAdapter,一個ValidationArttribute(Range繼承自此)和ValidationContextRangeAttribute只是指定值的位置,並且有很多,而不僅僅是一個簡單的屬性。

其他的東西,如PostSharp是「織」 - 很簡單的使用(種),但他們重寫代碼注入數額是多少,包裝監視屬性的更改,並致電範圍驗證方法(S)。然後進行更多的反思,將驗證的數據發回給該屬性。

重點:你所看到的東西都不是只是屬性,還有更多的事情正在進行。

下面是一個RangeManager,它不像韋弗那樣透明,但它更簡單。核心是一個範圍屬性,您可以指定有效的最小/最大值。但是也需要創建一個RangeManager對象,它將完成尋找屬性的繁重工作,從setter方法轉換,找到有效範圍並進行測試。它掃描它被實例化的類型以查找所有相關屬性。

這是反射調用節儉。當實例化時,管理器查找所有標記的屬性並保存對實例的引用,以便每次設置屬性時都不會調用幾個新的反射方法。

如果沒有其他,它顯示了一些涉及什麼。

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 

由於代碼中的註釋說明,你可以繼承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 

對我來說,唯一能讓它超越我的阻礙因素就卸載數據驗證而言ñ以外的物業類是使用的範圍列在屬性上方。


Attributes對它們裝飾的屬性一無所知。這是其他作出該連接,並且其他將需要使用反射來獲取Attribute數據。

同樣,性質一無所知分配給他們,因爲Attributes是用於編譯器或別的東西像串行元數據Attributes。此其他還必須使用反射來建立兩層(類型方法和元數據)之間的連接。

最後,Something Else最終可能是重寫您的發射組件以提供範圍檢查服務的工具,或者是通過如上所示的方法提供服務的庫。

更透明的事情的障礙是沒有像PropertyChanged事件掛鉤(請參閱PropertyInfo)。

  • 用於實現IComparable在RangeCheck
+0

'我不認爲你想在值超出範圍時拋出異常'爲什麼不呢?在調試的情況下或發現一個範圍的故障可能是非常有用的,ASP.Net的'RangeAttribute'具有'Throwexception'功能(我不記得確切的參數名稱),可以選擇設置帶有可選的異常消息。 – ElektroStudios 2014-10-01 21:26:12

+0

關於代碼,首先是Thankyou,但這確實是我想避免的,我的意圖是簡化屬性以避免寫入getter/setter,然後只附加range屬性。如果你注意到我所顯示的第一個代碼和你的最後一個代碼基本相同,我的沒有反射和你的反射,我真的沒有看到反射方法的任何好處,你正在評估setter中的值,是同樣的事情,但更重的方式。感謝您的回答 – ElektroStudios 2014-10-01 21:28:21

+0

請參閱此問題中使用'PostSharp'的解決方案:http://stackoverflow.com/questions/20019130/minvalue-maxvalue-attribute-for-properties,因爲您看到該屬性中唯一的請求是「attach 「範圍屬性,沒有getter,也沒有setter,我不知道'LocationInterceptionAspect'到底是什麼,或者我可以如何做類似的事情,但這意味着'yes可以做些什麼'的屬性,然後一旦發現如何僅將屬性頂部的範圍attrib(以及範圍屬性的類)添加,getter/setter就不是必需的。謝謝 – ElektroStudios 2014-10-01 21:44:47