Wednesday, 28 January 2009

Implementing Observable Objects – Part III

In Part II we saw the introduction of the ObservableItem base class, which simplifies the property update mechanism by placing all of the boilerplate code in the SetProperty method. The problem with this approach is that it’s too easy to make a mistake with the property names, so now we’re going to reinforce the SetProperty method by asserting the relevant assumptions. These assumptions are:

  • A property with the specified name exists on the type.
  • The invoked method is the property setter for the property with the specified name.

To facilitate this we need to restructure the SetProperty method so that both overloads funnel through to the same method. I’ll explain why this is necessary in more detail later, but for now here’s the new structure:

protected void SetProperty<T>(string propertyName, ref T field, T value)
{
  SetPropertyInternal(propertyName, ref field, value, EqualityComparer<T>.Default);
}

protected void SetProperty<T>(
  string propertyName, ref T field, T value, IEqualityComparer<T> comparer)
{
  SetPropertyInternal(
    propertyName, 
    ref field, 
    value, 
    comparer == null ? EqualityComparer<T>.Default : comparer);
}

private void SetPropertyInternal<T>(
  string propertyName, ref T field, T value, IEqualityComparer<T> comparer)
{
  DebugVerifySetPropertyInternalArguments(propertyName, comparer);
  if (!comparer.Equals(field, value))
  {
    field = value;
    OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
  }
}

Not a huge change as you can see. The implementation is essentially the same although there’s a new method – DebugVerifySetPropertyInternalArguments – called before the equality check. This is the method that contains the relevant asserts.

[Conditional("DEBUG")]
private void DebugVerifySetPropertyInternalArguments<T>(
  string propertyName, IEqualityComparer<T> comparer)
{
  Debug.Assert(
    comparer != null, 
    "Argument null.", 
    "Argument 'comparer' should not be a null reference.");
  var type = GetType();
  var propertyType = typeof(T);
  PropertyInfo property = type.GetProperty(propertyName, propertyType);
  if (property == null)
  {
    Debug.Fail(
      "Argument invalid.",
      string.Format(
        CultureInfo.InvariantCulture,
        "Argument 'propertyName' does not represent the name of a property on this " +
        "type with the expected return type. One or more of the following values " +
        "is wrong.\n\nType: {0}\nProperty name: {1}\nProperty type: {2}",
        type.FullName, propertyName, propertyType.FullName));
  }
  else
  {
    MethodBase expectedSetMethod = property.GetSetMethod();
    var frame = new StackFrame(3);
    MethodBase actualSetMethod = frame.GetMethod();
    Debug.Assert(
      actualSetMethod == expectedSetMethod,
      "Argument invalid.",
      string.Format(
        CultureInfo.InvariantCulture,
        "Argument 'propertyName' does not match the name of the property being " +
        "set.\n\nExpected property set method: {0}\nActual property set method: {1}",
        expectedSetMethod, actualSetMethod));
  }
}

The first assert verifies that we have a comparer, otherwise we have no way of knowing whether or not the value has changed. The second assert verifies that the type has a property with the correct name and return type. If you make a mistake like specifying a non-existent property name, then you’ll see the following message under the debugger:

Property Not Found Assert

The final assert verifies that once we have identified a property with the correct name and return type, the property’s set method matches the method being invoked. This is why we had to restructure the overloads, because the invoked method is retrieved from a StackFrame. Having all of the SetProperty overloads call SetPropertyInternal means we always know how many frames to skip to get to the property setter. If you make a mistake like specifying an existing property name in the wrong property setter, then you’ll see the following message under the debugger:

Invalid Property Setter Assert

Both of these messages are extremely useful for quickly and accurately tracking down typos and copy/paste errors in debug builds.

The observant among you will no doubt have spotted that there are still a few issues. One of which is that the ObservableItem class is public and the SetProperty overloads are therefore part of its public API. Now it’s entirely possible for your data model assembly to be a private assembly, i.e. an assembly that isn’t intended or authorised to be used by third parties. When this isn’t the case however, we can’t rely on the use of asserts because third parties are unlikely to be using debug builds of your data model. Another worry is that we can conveniently ignore the overhead of using reflection and interrogating the call stack with the asserts because they’ll only execute in debug builds. We really don’t won’t to incur this overhead at such a low level in our data model for release builds. In Part IV we’ll look at an approach that provides the same level of diagnostic assistance as the asserts described here but without requiring access to a debug build of the data model and without unnecessarily penalising the release build performance.

0 comments:

Post a Comment