ObservableCollection Property Class

2020-02-13 06:01发布

问题:

I repeat this property in my MVVM project too many times to count. What would be the correct way to create a generic class or factory to replace these lines of code?

    ObservableCollection<Result> _resultCollection;
    public ObservableCollection<Result> ResultCollection
    {
        get
        {
            if (_resultCollection == null)
                _resultCollection = new ObservableCollection<Result>();
            return _resultCollection;
        }
        set
        {
            _resultCollection = value;
        }
    }

回答1:

I know this doesn't exactly answer your question, however personally I prefer to use a recorded macro in Visual Studio that writes all that for me.

I prefer using this to a generic class as it keeps all the relevant code in one place, and it's easy to understand what is going on. Typically I put all my private Fields at the top of my class, and hide all the public Properties in a #region tag that I keep collapsed so I don't need to scroll through them.

It's fairly easy to create a macro in VS: Simply go to Tools > Macros > Record Temporary Macro, then perform the changes you want using the keyboard only. Once you have the Macro working correctly, simply save it as a permanent macro. If you do it right, you can re-run the macro with any variable and it will build it out the same way.

Some useful keyboard shortcuts to remember when creating macros are:

  • Ctrl+C/Ctrl+V to copy/paste
  • Ctrl+Right/Ctrl+Left to move over a word
  • Home/End to move to the beginning or end of a line
  • Shift for highlighting words as you move the cursor

You may also need to do some minor modifications to the VB macro code, such as a .Replace() to turn lowercase letters to capitals.

Here's an example Macro I use to build out all my public Properties for MVVM. It uses the PRISM library, so it uses the syntax RaisePropertyChanged(() => this.SomeProperty);

Sub PRISM_BuildPropertyChanged_CursorAtDefaultAfterCtrlRE()
    DTE.ActiveDocument.Selection.LineDown(False, 2)
    DTE.ActiveDocument.Selection.WordLeft(True)
    DTE.ActiveDocument.Selection.Copy()
    DTE.ActiveDocument.Selection.LineDown(False, 3)
    DTE.ActiveDocument.Selection.EndOfLine()
    DTE.ActiveDocument.Selection.CharLeft()
    DTE.ActiveDocument.Selection.NewLine()
    DTE.ActiveDocument.Selection.Text = "RaisePropertyChanged(() => this."
    DTE.ActiveDocument.Selection.Paste()
    DTE.ActiveDocument.Selection.Text = ");"
    DTE.ActiveDocument.Selection.LineUp()
    DTE.ActiveDocument.Selection.StartOfLine(vsStartOfLineOptions.vsStartOfLineOptionsFirstText)
    DTE.ActiveDocument.Selection.CharRight(False, 4)
    DTE.ActiveDocument.Selection.NewLine()
    DTE.ActiveDocument.Selection.CharRight()
    DTE.ActiveDocument.Selection.NewLine()
    DTE.ActiveDocument.Selection.Text = "if (value != "
    DTE.ActiveDocument.Selection.WordRight(True)
    DTE.ActiveDocument.Selection.Copy()
    DTE.ActiveDocument.Selection.CharLeft()
    DTE.ActiveDocument.Selection.Paste()
    DTE.ActiveDocument.Selection.DeleteLeft()
    DTE.ActiveDocument.Selection.Text = ")"
    DTE.ActiveDocument.Selection.NewLine()
    DTE.ActiveDocument.Selection.Text = "{"
    DTE.ActiveDocument.Selection.LineDown()
    DTE.ActiveDocument.Selection.EndOfLine()
    DTE.ActiveDocument.Selection.NewLine()
    DTE.ActiveDocument.Selection.Text = "}"
End Sub

With it, I can write my private definition, such as

private ObservableCollection<SomeObject> _someCollection;

Hit Ctrl+R, E to build the public property definition

private ObservableCollection<SomeObject> _someCollection;

public ObservableCollection<SomeObject> SomeCollection
{
    get { return _someCollection; }
    set { _someCollection = value; }
}

Then run my Macro to build out the Property Change notification

public ObservableCollection<SomeObject> SomeCollection
{
    get { return _someCollection; }
    set 
    {
        if (value != _someCollection)
        {
            _someCollection = value;
            RaisePropertyChanged(() => this.SomeCollection);
        }
    }
}

(By default, Ctrl+R, E puts the cursor at the end of the private field definition, so that is where the cursor needs to be when running this macro)

I also have another old macro that adds code that checks if the value is null, and if so sets it to a new instance of the object, but its one I never used much since I prefer to set my default definitions in the Constructor instead of in the Property definition.

If you want it, it looks like this (note the Macro name - You need to highlight the public property name prior to running this macro):

Sub CreatePublicGet_NoNull_HightlightPropertyNameFirst()
    DTE.ActiveDocument.Selection.CharLeft(False, 2)
    DTE.ActiveDocument.Selection.StartOfLine(vsStartOfLineOptions.vsStartOfLineOptionsFirstText, True)
    DTE.ActiveDocument.Selection.WordRight(True)
    DTE.ActiveDocument.Selection.Copy()
    DTE.ActiveDocument.Selection.LineDown(False, 2)
    DTE.ActiveDocument.Selection.StartOfLine(vsStartOfLineOptions.vsStartOfLineOptionsFirstText)
    DTE.ActiveDocument.Selection.CharRight(False, 4)
    DTE.ActiveDocument.Selection.NewLine()
    DTE.ActiveDocument.Selection.CharRight()
    DTE.ActiveDocument.Selection.NewLine()
    DTE.ActiveDocument.Selection.EndOfLine()
    DTE.ActiveDocument.Selection.CharLeft()
    DTE.ActiveDocument.Selection.NewLine()
    DTE.ActiveDocument.Selection.LineUp(False, 2)
    DTE.ActiveDocument.Selection.EndOfLine()
    DTE.ActiveDocument.Selection.NewLine()
    DTE.ActiveDocument.Selection.Paste()
    DTE.ActiveDocument.Selection.Text = "();"
    DTE.ActiveDocument.Selection.LineDown()
    DTE.ActiveDocument.Selection.WordLeft(True)
    DTE.ActiveDocument.Selection.CharLeft()
    DTE.ActiveDocument.Selection.WordLeft(True)
    DTE.ActiveDocument.Selection.Copy()
    DTE.ActiveDocument.Selection.LineUp(False, 2)
    DTE.ActiveDocument.Selection.NewLine()
    DTE.ActiveDocument.Selection.Text = "if ("
    DTE.ActiveDocument.Selection.Paste()
    DTE.ActiveDocument.Selection.Text = " == null)"
    DTE.ActiveDocument.Selection.NewLine()
    DTE.ActiveDocument.Selection.Text = "{"
    DTE.ActiveDocument.Selection.LineDown()
    DTE.ActiveDocument.Selection.StartOfLine(vsStartOfLineOptions.vsStartOfLineOptionsFirstText)
    DTE.ActiveDocument.Selection.EndOfLine()
    DTE.ActiveDocument.Selection.NewLine()
    DTE.ActiveDocument.Selection.Text = "}"
    DTE.ActiveDocument.Selection.LineUp()
    DTE.ActiveDocument.Selection.StartOfLine(vsStartOfLineOptions.vsStartOfLineOptionsFirstText)
    DTE.ActiveDocument.Selection.Paste()
    DTE.ActiveDocument.Selection.Text = " = new "
    DTE.ActiveDocument.Selection.Collapse()
    DTE.ActiveDocument.Selection.EndOfLine()
    DTE.ActiveDocument.Selection.LineDown(False, 4)
    DTE.ActiveDocument.Selection.EndOfLine()
    DTE.ActiveDocument.Selection.LineUp(True)
    DTE.ActiveDocument.Selection.Delete()
End Sub

And results in code looking like this:

public ObservableCollection<SomeObject> SomeCollection
{
    get 
    {
        if (_someCollection == null)
        {
            _someCollection = new ObservableCollection<SomeObject>();
        }
        return _someCollection; 
    }
}


回答2:

AFAIK it's not possible without some form of code generation. Look into T4 templates, as answered here .



回答3:

public abstract class XBase<T>
{
    ObservableCollection<T> _resultCollection;

    public ObservableCollection<T> ResultCollection
    {
        get
        {
            if (_resultCollection == null)
                _resultCollection = new ObservableCollection<T>();

            return _resultCollection;
        }
        set
        {
            _resultCollection = value;

        }
    }
}


回答4:

Yea... to be able to bind to a field would be great, wouldn't it... ;-) This brings me to the first solution:

(1) create a converter that allows you to bind a field.

public class FieldBindingConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        var par = parameter as string;
        var field = value.GetType().GetField(par);
        return field.GetValue(value);
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

and then (2) binding the field directly from XAML:

    <ItemsControl ItemsSource="{Binding ., Converter={StaticResource Field}, ConverterParameter=Coll2}" Grid.Row="1">

I kinda dislike this to be honest because it doesn't really clear things up... So as the second solution, you can instantiate the property directly from the constructor:

public Foo() 
{
    ResultCollection = new ObservableCollection<Result>();
}

[...]

public ObservableCollection<Result> ResultCollection { get; private set; }

While this does increase your memory footprint a few bytes (it changes the semantics of lazy instantiation into direct instantiation), I wouldn't be too concerned about that.