A better way to write MVVM boilerplate code?

2019-04-07 18:17发布

问题:

I have found myself recently writing a lot of boilerplate MVVM code and wonder if there is a fancy way to get around writing it all? I already use a ViewModelBase class that implements INotifyPropertyChanged but that doesnt solve the problem of having to write all the accessor code etc. Perhaps by writing a custom attribute that does this, or via a templating system?

public MyClass : ViewModelBase
{
    private int someVariable;

    public int SomeVariable
    {
        get
        {
            return this.someVariable;
        }

        set
        {
            this.someVariable = value;
            this.NotifyPropertyChanged("SomeVariable");
        }
    }
}

回答1:

I have a snippet that i use to create my view model properties. This particular snippet uses the Expression<Func<T>> notation that other commenters have hinted upon.

<?xml version="1.0" encoding="utf-8"?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
  <CodeSnippet Format="1.0.0">
    <Header>
      <SnippetTypes>
        <SnippetType>Expansion</SnippetType>
      </SnippetTypes>
      <Title>View Model Property</Title>
      <Description>
          Declares a property and member suitable for Viewmodel implementation.
      </Description>
      <HelpUrl>
      </HelpUrl>
      <Shortcut>propvm</Shortcut>
    </Header>
    <Snippet>
      <Declarations>
        <Literal Editable="true">
          <ID>propname</ID>
          <ToolTip>Property Name</ToolTip>
          <Default>Name</Default>
          <Function>
          </Function>
        </Literal>
        <Literal Editable="true">
          <ID>type</ID>
          <ToolTip>Property type.</ToolTip>
          <Default>Type</Default>
          <Function>
          </Function>
        </Literal>
        <Literal Editable="true">
          <ID>init</ID>
          <ToolTip>Member initialisation</ToolTip>
          <Default>null</Default>
          <Function>
          </Function>
        </Literal>
      </Declarations>
      <Code Language="csharp" Kind="type decl"><![CDATA[public $type$ $propname$
{
    get { return m_$propname$; }
    set 
    { 
        m_$propname$ = value;
        base.OnPropertyChanged(() => $propname$);
    }
} $type$ m_$propname$ = default($type$);$end$]]></Code>
    </Snippet>
  </CodeSnippet>
</CodeSnippets>

Note the call to base.PropertyChanged(). I have a ViewModelBase class to do the heavy lifting of property notification and validation for me.

Usage is this:

  1. Type propvm
  2. Hit TAB twice
  3. Fill in the highlighted field and press tab to flip to the next one!

Walkthrough : Creating a code snippet



回答2:

Aspect oriented programming (AOP) is a way to reduce the amount of such boilerplate code. A framework that is widely known is PostSharp. There also is a free Express edition.
You use attributes (either on the classes directly or as a multicast to all points in code that satisfy a specific set of conditions) to mark the spots where the code should be integrated and PostSharp weaves in the implementations during build. You can find an example for the implementation of INotifyPropertyChanged here.
An AOP based approach (no matter which framework you use) has the advantage that you can change the implementation afterwards and that these changes are reflected in the existing code base. It is also possible to apply the aspects to a big number of already existing classes.



回答3:

Wow... that's a lot of answering in the comments and not so much in the answers. As an alternative to the lovely new CallerMemberNameAttribute attribute, how about Visual Studio Macros? I have a number of these that fully implement all of my interfaces (both custom and .NET) for me at the click of a mouse button.

Downside to using Macros:

You write them using Visual Basic
It can take a while to write a long one
They can contain errors like any code

Upside to using Macros:

You can 'record' simple Macros as you type
You can build up complex Macros that can work with current context
They can write thousands of words at the click of a mouse button

For example, I can create a class file, defining just the class name, base class and/or interfaces. After declaring the private member variables, I can run my custom Macro and it will read the names and types of the variables and generate constructors, properties and all methods required by the base classes and/or interfaces used. However, this particular Macro is almost 600 lines long.



回答4:

First of all and as mentioned already, use a snippet to create the code for you. Then there are a couple of libraries that can help you with it, or AOP.

And here's something I've been using for a while in applications where raw ui performance on simple controls is irrelevant: a helper class with a Dictionary<string,object> to hold the actual property backends, and methods to get/set properties of any type, taking an expression as argument in order to avoid using string literals. When using this, a property comes down to

public int SomeProperty
{
  get { return properties.Get( model => model.SomeProperty ); }
  set { properties.Set( model => model.SomeProperty, value ); }
}

also that Set call returns true when the value really changed as that is often useful.

Here's some code, with the usual 'use at own risk' etc warning. You just need a NotifyPropertyChangedHelper implementation but that can be found easily (search the net for 'propertychanged helper' for instance, pretty sure it was posted on SO as well)

public class NotifyPropertyChangedMap<T> where T : INotifyPropertyChanged
{
  #region Fields
  private readonly T propertyContainer;
  private readonly Dictionary<string, object> properties;
  #endregion

  #region Constructors
  public NotifyPropertyChangedMap( T propertyContainer )
  {
    Contract.Requires<ArgumentNullException>( propertyContainer != null, "propertyContainer" );

    this.propertyContainer = propertyContainer;
    this.properties = new Dictionary<string, object>();
  }
  #endregion

  #region Get and Set
  public Property Get<Property>( Expression<Func<T, Property>> expression )
  {
    var propName = NotifyPropertyChangedHelper.GetPropertyName( expression );
    if( !properties.ContainsKey( propName ) )
      properties.Add( propName, GetDefault<Property>() );
    return (Property) properties[ propName ];
  }

  public bool Set<Property>( Expression<Func<T, Property>> expression, Property newValue )
  {
    var propName = NotifyPropertyChangedHelper.GetPropertyName( expression );
    if( !properties.ContainsKey( propName ) )
    {
      properties.Add( propName, newValue );
      propertyContainer.RaisePropertyChangedEvent( propName );
    }
    else
    {
      if( EqualityComparer<Property>.Default.Equals( (Property) properties[ propName ], newValue ) )
        return false;
      properties[ propName ] = newValue;
      propertyContainer.RaisePropertyChangedEvent( propName );
    }
    return true;
  }
  #endregion

  #region Implementation
  private static Property GetDefault<Property>()
  {
    var type = typeof( Property );
    return (Property) ( type.IsValueType ? Activator.CreateInstance( type ) : null );
  }
  #endregion
}


回答5:

Use a snippet like "mvvmprop" There are many out there that are available already written just for this purpose, including MVVM Lite's implementation.