围绕方面和字段注入的设计约定

Design convention around aspects and field injection

假设我们有一段围绕 GridControl 的典型表单代码。

private GridControl myGrid;

internal void InitialiseGrid()
{
    myGrid.BeginUpdate();
    try
    {
        ....
    }
    finally
    {
        myGrid.EndUpdate();
    }
}

现在假设我想使用 PostSharp 或其他工具将此类行为包装在横切中,以便最终代码看起来类似于:

private GridControl myGrid;

[MyGridControlUpdateAspect(FieldName="myGrid")]
internal void InitialiseGrid()
{
    ....
}

鉴于 SO 和其他地方不断反对不使用反射来访问 类 中的私有字段,任何人都可以提供更好的方法来访问 myGrid 并在方面源代码中调用 BeginUpdate 和 EndUpdate 方法,以这种方式可以将对特定网格的引用以某种方式传递给方面,并且仍然满足纯粹主义者的要求。

更新:以下是一个真实世界的示例,该示例包含在 try/finally 块中用于在进入方法时更改光标的代码。通过利用一个方面来执行此功能,我可以将此功能添加到许多可能需要时间的方法中,而无需专门将此功能添加到任何特定代码段中。

[ChangeCursor(CursorPropertyName = "Cursor", NewCursorTypeName = "WaitCursor", AspectPriority = 8)]
internal void SomeButtonClick(object sender, System.EventArgs args)...

[assembly: ChangeCursor(CursorPropertyName = "Cursor", NewCursorTypeName = "WaitCursor", AttributeTargetTypes = "SomeNamespace.*", AttributeTargetMembers = "regex:.*ButtonClick", AttributePriority = 30, AspectPriority = 12)]

方面代码(注意反射的使用——在本例中它使用实际实例而不是实例中的字段,但概念是相同的)。

/// <summary>
/// Aspect to set the cursor for a windows form to a particular
/// cursor type and reset it back to the default type on exit
/// </summary>
[Serializable]
[AttributeUsage(AttributeTargets.Method)]
[MulticastAttributeUsage(MulticastTargets.Method)]
public sealed class ChangeCursorAttribute : OnMethodBoundaryAspect
{
    /// <summary>
    /// The name of the property that will be available in the instance
    /// of the method that this aspect advises.
    /// <para>It is expected to derive from System.Windows.Forms but
    /// does not necessarily have to provided it has a System.Windows.Form.Cursor property
    /// that matches this name</para>
    /// </summary>
    public string CursorPropertyName { get; set; }

    /// <summary>
    /// The name of the cursor to set to a standard System.Windows.Forms.Cursors type
    /// </summary>
    public string NewCursorTypeName { get; set; }

    /// <summary>
    /// The type of the cursor to set on entry
    /// </summary>
    private Cursor NewCursorType { get; set; }

    /// <summary>
    /// The property info for the cursor property name
    /// </summary>
    private PropertyInfo CursorPropertyInfo { get; set; }

    /// <summary>
    /// The aspect is advising on an extension method
    /// instead of a method in the class with the Cursors attribute
    /// </summary>
    private bool IsExtensionMethodAttribute { get; set; }

    /// <summary>
    /// Validate the necessary properties are set in the attribute at compile time
    /// </summary>
    /// <param name="method"></param>
    /// <returns></returns>
    public override bool CompileTimeValidate(MethodBase method)
    {
        if (CursorPropertyName == null)
            throw new InvalidAnnotationException(string.Format("CursorPropertyName must be defined: {0}.{1}", method.DeclaringType.FullName, method.Name));
        if (NewCursorTypeName == null)
            throw new InvalidAnnotationException(string.Format("NewCursorType must be defined: {0}.{1}", method.DeclaringType.FullName, method.Name));
        return base.CompileTimeValidate(method);
    }

    /// <summary>
    /// Initialise the information required for this attribute
    /// at runtime
    /// </summary>
    /// <param name="method"></param>
    public override void RuntimeInitialize(MethodBase method)
    {
        base.RuntimeInitialize(method);
        PropertyInfo pi = typeof(Cursors).GetProperty(NewCursorTypeName, BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy);
        NewCursorType = (Cursor)pi.GetValue(null, null);

        try
        {
            // If attribute associated with extension method use the type of the 
            // first parameter to associate the property with
            if (method.IsDefined(typeof(ExtensionAttribute), false))
            {
                ParameterInfo paramInfo = method.GetParameters()[0];
                Type type1 = paramInfo.ParameterType;
                CursorPropertyInfo = type1.GetProperty(CursorPropertyName);
                IsExtensionMethodAttribute = true;
            }
            else
                CursorPropertyInfo = method.DeclaringType.GetProperty(CursorPropertyName);
        }
        catch (Exception ex)
        {
            throw new InvalidAnnotationException(string.Format("CursorPropertyName {2} not found in type: {0}.{1}\n{3}\n", method.DeclaringType.FullName, method.Name, CursorPropertyName, ex.GetType().FullName, ex.Message));
        }
    }

    /// <summary>
    /// On entry to a method set the cursor type to the required
    /// type as specified in the attribute arguments
    /// </summary>
    /// <param name="args">The arguments to the method</param>
    public override sealed void OnEntry(MethodExecutionArgs args)
    {
        CursorPropertyInfo.SetValue(GetInstance(args), NewCursorType, null);
    }

    /// <summary>
    /// On method exit, regardless of success or failure reset
    /// the form cursor to the default cursor type
    /// </summary>
    /// <param name="args">The arguments to the method</param>
    public override sealed void OnExit(MethodExecutionArgs args)
    {
        CursorPropertyInfo.SetValue(GetInstance(args), Cursors.Default, null);
    }

    /// <summary>
    /// Get the object instance that contains the Cursor property
    /// depending on whether this attribute is attached to a method 
    /// within a class or an extension method
    /// </summary>
    /// <param name="args">The arguments to the method</param>
    /// <returns>The instance object</returns>
    private object GetInstance(MethodExecutionArgs args)
    {
        object instance = args.Instance;
        if (IsExtensionMethodAttribute)
            instance = args.Arguments[0];
        return instance;
    }
}

虽然通过反射访问私有字段通常不是一个好的做法(并且可能无法在受限的安全设置中工作),但您必须记住,使用反射的 (PostSharp) 方面代码通常仅在编译时运行。 PostSharp 使用反射 API 是为了方便,因为用户对此很熟悉。

在第一个示例中,问题是按名称引用字段,这可能对重构工具不透明并且通常不干净。在这种情况下,要解决这个问题会有点困难 - 最后我只会勾勒出解决方案。

在第二个示例中,您在 RuntimeInitialize 中使用了反射,这是所谓的纯粹主义者会批评的。可以减少反射和方面参数计数。 PostSharp 允许您使用 IAspectProvider interface and IAdviceProvider 接口动态引入方面。

有关从运行时删除不必要的反射的演示,请参见以下内容:

[Serializable]
[IntroduceInterface(typeof(ICursorProperty))]
public class CursorPropertyTypeAttribute : TypeLevelAspect, ICursorProperty, IAdviceProvider, IInstanceScopedAspect
{
    public Property<Cursor> Cursor;

    Cursor ICursorProperty.Cursor { get { return Cursor.Get(); } set { Cursor.Set(value); } }

    public IEnumerable<AdviceInstance> ProvideAdvices( object targetElement )
    {
        yield return new ImportLocationAdviceInstance(this.GetType().GetField("Cursor", BindingFlags.Public | BindingFlags.Instance), this.FindCursorProperty((Type)targetElement));
    }

    public LocationInfo FindCursorProperty(Type targetType)
    {
        foreach ( PropertyInfo property in targetType.GetProperties(BindingFlags.Public | BindingFlags.Instance) )
        {
            if ( null != property.GetCustomAttribute( typeof(CursorPropertyAttribute) ) )
                return new LocationInfo( property );
        }

        return null;
    }

    public object CreateInstance(AdviceArgs adviceArgs)
    {
        return this.MemberwiseClone();
    }

    public void RuntimeInitializeInstance()
    {
    }
}

public interface ICursorProperty
{
    Cursor Cursor { get; set; }
}

[Serializable]
[AspectTypeDependency(AspectDependencyAction.Order, AspectDependencyPosition.After, typeof(CursorPropertyTypeAttribute))]
public class ChangeCursorAttribute : OnMethodBoundaryAspect, IAspectProvider
{
    private string cursorName;

    [NonSerialized]
    private Cursor cursor; 

    public ChangeCursorAttribute( string cursorName )
    {
        this.cursorName = cursorName;
    }

    public IEnumerable<AspectInstance> ProvideAspects(object targetElement)
    {
        Type type = ((MethodBase) targetElement).DeclaringType;
        IAspectRepositoryService repository = PostSharpEnvironment.CurrentProject.GetService<IAspectRepositoryService>();

        if ( !repository.HasAspect( type, typeof(CursorPropertyTypeAttribute) ) )
            yield return new AspectInstance( type, new CursorPropertyTypeAttribute() );
    }

    public override void CompileTimeInitialize( MethodBase method, AspectInfo aspectInfo )
    {
        if ( null == typeof(Cursors).GetProperty( this.cursorName, BindingFlags.Public | BindingFlags.Static ) )
            MessageSource.MessageSink.Write( new Message( MessageLocation.Of( method ), SeverityType.Error, "USR001", "Invalid cursor name", null, "MyComponent",
                null ) );
    }

    public override void RuntimeInitialize( MethodBase method )
    {
        this.cursor = (Cursor) typeof(Cursors).GetProperty( this.cursorName, BindingFlags.Public | BindingFlags.Static ).GetValue( null );
    }

    public override void OnEntry(MethodExecutionArgs args)
    {
        (args.Instance as ICursorProperty).Cursor = cursor;
    }

    public override void OnExit(MethodExecutionArgs args)
    {
        (args.Instance as ICursorProperty).Cursor = Cursors.DefaultCursor;
    }
}

有两个方面 - 第一个是引入目标 class 的接口,该接口将用于获取 Cursor 属性 值。第二个方面适用于方法。在编译时,它确保第一个出现在类型上并检查目标游标是否存在。在运行时,它通过界面设置光标而不进行任何反射。只有运行时反射从 public static 属性 获取光标(为简洁起见)。

为了给您一些启发,您可以使用 PostSharp 进行更高级的转换,以更简洁的方式实现您正在做的事情,从而消除按名称引用的问题。请参阅 ISyntaxReflectionService 接口,它允许您获取方法的抽象语法树(CIL,而不是 C#)。您可以使用此接口来分析方法并决定在哪些字段上需要调用 BeginUpdateEndUpdate.