`Window` `Width` and `Top` have local values

54 Views Asked by At

My main window's Height, Left, Top, and Width are all bound to their respective viewmodel properties through a style. I can confirm that these four properties in the view model are only ever set to 1920, 1920, 118, 1080 respectively.

But when I launch the app, the Top and Width properties on the main window are set to something else (Width will be 1440 and Top will be a random number usually less than 300). What would cause this?

Here's what I see when I Snoop the app. Notice how Top and Width come from a Local Value Source:

Before clear/reset

Strangely, when I right-click on those properties in Snoop and tell it to "Clear/Reset", then those properties begin behaving. What is Snoop doing that fixes this?

After clear/reset

Other facts:

  • The getters for the Top and Width viewmodel properties are only called once while the main window is being initialized. The stack trace runs through framework binding initialization code.
  • The setters for the Top and Width viewmodel properties are only called once from the viewmodel constructor as it sets those properties to 118 and 1080 respectively.
  • The bindings for these four properties are all two-way.
  • None of these things cause the view's properties to change/be correct:
    • Changing the associated viewmodel properties at runtime, even after the view has been fully loaded.
    • Calling UpdateLayout() on the view.
    • Calling InvalidateArrange() on the view.
    • Calling InvalidateMeasure() on the view.
    • Calling InvalidateProperty(FrameworkElement.WidthProperty) on the view.
    • Calling InvalidateVisual() on the view.
  • I have searched and searched and do not see any code anywhere touching the view's Top or Width properties (other than the style bindings).

Here's the style:

Bindings in the style

Sorry I had to blank out type names and some other things—it's a company application. If it helps, the main window/the view is at the end of a long inheritance line with Window as its great-great grandaddy. I'm trying to make the main window more reusable by MVVM-ing it—formerly these layout properties were set in code-behind in the view, and the view had constructor parameters :'( That's related to why I need to key the style, and why the style is based on other stuff. But none of the inherited types manipulate layout properties.

P.S. I've seen other people complain about how hard it is to resize WPF's Window. The most commonly suggested solution is to bind MinWidth and MaxWidth as well as Width. When I do that then the Width is indeed forced to the value I want, but you can't resize the window, the Width property still has its Local Value Source, and Top is still incorrect.

1

There are 1 best solutions below

0
Matt Thomas On

Given that "[Top] cannot be set through a style", and given the complications with binding Window.Width, I solved this a different way.

I created this attached property called WindowLayout and bound it to a viewmodel property in my style:

public static class WindowLayoutBehavior
{
    public static readonly DependencyProperty LayoutProperty = DependencyPropertyHelpers.RegisterAttached(
        (Window x) => GetLayout(x),
        new PropertyMetadata(HandleLayoutChanged));

    private static void HandleLayoutChanged(
        DependencyObject d,
        DependencyPropertyChangedEventArgs e)
    {
        if (!(d is Window window))
            return;
        if (!(e.NewValue is Rect rect))
            return;
        window.Height = rect.Height;
        window.Left = rect.Left;
        window.Top = rect.Top;
        window.Width = rect.Width;
    }

    [AttachedPropertyBrowsableForType(typeof(Window))]
    public static Rect GetLayout(Window window) =>
        window.GetValue(LayoutProperty) is Rect rect
            ? rect
            : default;

    public static void SetLayout(Window window, Rect rect) =>
        window.SetValue(LayoutProperty, rect);
}

DependencyPropertyHelpers.RegisterAttached is a shorthand helper method for creating the attached property in the way you might expect.

Usage in the style:

<Setter
    Property="WindowLayoutBehavior.Layout"
    Value="{Binding WindowLayout, Mode=OneWay}"/>

Now when I Snoop the app, Height, Left, Top, and Width all show as having Local Value Sources, and they change when the viewmodel property changes, so that works for me.