Search code examples
c#wpfxamlbindingmarkup-extensions

WPF - Provide Design-Time Value for Custom Markup Extension with RelativeSource Binding


N.B.: THIS IS NOT JUST ABOUT THE CUSTOM MARKUP EXTENSIONS. PLEASE READ BEFORE MARKING AS DUPLICATE.

I have a WPF markup extension with a converter, and the two of them go as follows:

  [ValueConversion(typeof(WindowState), typeof(object))]
  internal class WindowStateToObjectConverter : IValueConverter {
    public WindowStateToObjectConverter() { }

    public WindowStateToObjectConverter(object maximized, object normal) {
      this.maximized = maximized;
      this.normal = normal;
    }

    #region Properties
    #region Maximized Property
    private object maximized;

    public object Maximized {
      get { return maximized; }
      set { maximized = value; }
    }
    #endregion
    #region Normal Property
    private object normal;

    public object Normal {
      get { return normal; }
      set { normal = value; }
    }
    #endregion
    #endregion

    public object Convert(object value, Type targetType, object param, CultureInfo culture) {
      if((value as WindowState? ?? WindowState.Normal) == WindowState.Maximized) return maximized;
      else return normal;
    }

    public object ConvertBack(object value, Type targetType, object param, CultureInfo culture) {
      throw new InvalidOperationException("Cannot convert downwards to WindowState");
    }
  }

  [MarkupExtensionReturnType(typeof(Binding))]
  internal class WindowMaximizedSwitchExtension : MarkupExtension {
    object maximized, normal;

    public WindowMaximizedSwitchExtension(object maximized, object normal) {
      this.maximized = maximized;
      this.normal = normal;
    }

    public override object ProvideValue(IServiceProvider serviceProvider) {
      Binding ret = new Binding("WindowState");
      RelativeSource retRSource = new RelativeSource(RelativeSourceMode.FindAncestor);
      retRSource.AncestorType = typeof(Window);
      ret.RelativeSource = retRSource;
      ret.Converter = new WindowStateToObjectConverter(maximized, normal);
      return ret.ProvideValue(serviceProvider);
    }
  }

They are for a custom window I am designing - they will be used to switch certain values (border width, margins, etc.) when the window is maximized. However, at design-time, they always return null, which is a real pain, because then my window looks like this:

Preview of the window in Visual Studio

...when it's supposed to look like this:

The window at runtime

(Ignore that the title and icon are missing in the first image, although, if you have a solution, feel free to answer - it's essentially the same problem.)

For obvious reasons, it would be extremely hard to design with this. The only major issue you see with the window preview is the places where I've used the extension to set Row/ColumnDefinitions - when it returns null, the Height/Width is set to 1*. So, my question is whether there is a way to select a default value, perhaps instead of the binding (e.g. the non-maximized value), at design time.


Solution

  • Well, I feel like an idiot, but I found the solution fairly quickly:

    In the ProvideValue method of the expression, I added the following line:

      ret.FallbackValue = normal;
    

    Where normal is the value to use when the window is not maximized.

    ProvideValue now looks like this:

    public override object ProvideValue(IServiceProvider serviceProvider) {
      Binding ret = new Binding("WindowState");
      RelativeSource retRSource = new RelativeSource(RelativeSourceMode.FindAncestor);
      retRSource.AncestorType = typeof(Window);
      ret.RelativeSource = retRSource;
      ret.Converter = new WindowStateToObjectConverter(maximized, normal);
      ret.FallbackValue = normal;
      return ret.ProvideValue(serviceProvider);
    }
    

    This returns the normal value during design-time.