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

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:
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?
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:
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.

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.

Related

Keeping aspect ratio of a control

I've looked for quite a while now for a way to be able to tell a WPF control (or window) to keep a certain aspect ratio.
For a Window I found this solution, that works quite well. But since it uses the Win32 API and window handles it's not working for any WPF Controls (because as far as I know in WPF only the window itself has a handle)
For a Control one usually gets the advice to put the Control in a ViewBox, but I don't want to scale my controls, I want them to resize (and keep any border width or font size).
Other "solutions" for a Control involve any form of binding the Width to the ActualHeight or the Height to the ActualWidth, or using the SizeChanged event, but this results in heavy flickering while resizing and it's not very reliable.
In case of binding the Width to the ActualHeight you can't resize only the Width (by dragging the right border) because the ActualHeight doesn't change.
In case of the event it gets tricky when width and height change at the same time, then you'd have to change the size inside the SizeChanged event... and did I mention the flickering?
After a lot of reading and searching I came to the conclusion that the best way to force any control to keep a certain aspect ratio would be to do that inside the Measure and Arrange functions.
I found this solution that creates a Decorator control with overridden Measure and Measure functions, but that would mean to put any control that's supposed to keep it's aspect ratio inside it's own Decorator. I could live with that if I had to, but I wonder if there's a better way to do it.
So, here's my question. Is it possible to create an attached property Ratio and an attached property KeepRatio and somehow override the Measure and Arrange functions of the controls in question in the OnKeepRatioChanged and RatioChanged callbacks of the attached properties?
If you want to override Arrange/Measure methods then there is no need in attached properties. This wrapper should be fine:
public partial class RatioKeeper : UserControl
{
public static readonly DependencyProperty VerticalAspectProperty = DependencyProperty.Register(
"VerticalAspect", typeof(double), typeof(RatioKeeper), new PropertyMetadata(1d));
public static readonly DependencyProperty HorizontalAspectProperty = DependencyProperty.Register(
"HorizontalAspect", typeof(double), typeof(RatioKeeper), new PropertyMetadata(1d));
public double HorizontalAspect
{
get { return (double) GetValue(HorizontalAspectProperty); }
set { SetValue(HorizontalAspectProperty, value); }
}
public double VerticalAspect
{
get { return (double) GetValue(VerticalAspectProperty); }
set { SetValue(VerticalAspectProperty, value); }
}
public RatioKeeper()
{
InitializeComponent();
}
//arrangeBounds provides size of a host.
protected override Size ArrangeOverride(Size arrangeBounds)
{
//Calculation of a content size that wont exceed host's size and will be of the desired ratio at the same time
var horizontalPart = arrangeBounds.Width / HorizontalAspect;
var verticalPart = arrangeBounds.Height / VerticalAspect;
var minPart = Math.Min(horizontalPart, verticalPart);
var size = new Size(minPart * HorizontalAspect, minPart * VerticalAspect);
//apply size to wrapped content
base.ArrangeOverride(size);
//return size to host
return size;
}
}

Why is a ListBoxItem not calling MeasureOverride when its width is changed?

Ok, for illustrative purposes, below I created a subclass of ListBoxItem and a subclass of ListBox which uses it as its container by overriding both IsItemItsOwnContainerOverride and GetContainerForItemOverride.
Now when the window first appears, as expected, MeasureOverride is called on every ListBoxItem (with Infinity,Infinity) followed by ArrangeOverride being called on every item.
However, when resizing the ListBox, only ArrangeOverride is called on the ListBoxItem, not MeasureOverride even though the metadata for the width property is set to AffectsMeasure.
NotE: I know I can get around this by setting ScrollViewer.HorizontalScrollbarVisibility to 'Disabled' in which case MeasureOverride does get called as expected because that scroll setting forces the items to match the width of the listbox and thus naturally would re-fire. However, I'm still trying to figure out why Measure isn't called by default anyway because the metadata for the Width property has the AffectsMeasure flag set and the width is changing via the ArrangeOverride step.
Is that flag just a hint for its container and in the case of a control placed in a ScrollViewer it's ignored? My guess is that unless you disable the scrolling, the controls have an infinite area available to them, so once they are measured, there's no need to re-measure them again. Disable the horizontal scrolling however and you're stating the width isn't unlimited, hence the MeasureOverride is called again. But that's just a guess, albeit a logical one.
Here's example code to play with. Create a new WPF project and paste this in the window's CodeBehind and look at the debug output. Next, set the HorizontalScrollbarVisibility flag and you'll see that it does get called.
public partial class MainWindow : Window
{
public MainWindow(){
InitializeComponent();
var items = new List<object>(){ "This is a really, really, really, really long sentence"};
var lbx = new ListBoxEx { ItemsSource = items };
this.Content = lbx;
}
}
public class ListBoxEx : ListBox
{
protected override bool IsItemItsOwnContainerOverride(object item){
return (item is ListBoxItemEx);
}
protected override DependencyObject GetContainerForItemOverride(){
return new ListBoxItemEx();
}
}
public class ListBoxItemEx : ListBoxItem
{
protected override Size MeasureOverride(Size availableSize){
Console.WriteLine("MeasureOverride called with " + availableSize);
return base.MeasureOverride(availableSize);
}
protected override Size ArrangeOverride(Size finalSize){
Console.WriteLine("ArrangeOverride called with " + finalSize);
return base.ArrangeOverride(finalSize);
}
}
Ok, I think the answer is it's not firing because the Measure pass is affecting the panel, not the items, and the panel itself hasn't resized so there's no reason to re-measure its children.
However, when setting ScrollViewer.HorizontalScrollbarVisibility to Disabled, the width of the panel tracks the width of the ListBox, not its contents, therefore the children do need to be re-measured, hence that's why they were in that case.

WPF TextBox recalculate size

When using a wpf textbox without explicit height and width values, and when there is space available to expand, the textbox resizes as you type.
However when I change the border thickness, it does not recalculate it and for very thick borders, part of the text is covered by the border. How do I explicitly precipitate a recalc?
Coincidently I am using a derived custom textbox class so I should know when the border thickness changes.
This bug must be some optimization gone wrong
Overriding Metadata for BorderThickness or adding a Dependency Property that affects Measure, Arrange or Render don't help
Undocking and Redocking from the parent container had no effect either
Even Undocking from the parent container and Redocking into a new container won't help if the space it is given in the new container is exactly the same as the space that it had in the old container
It seems like the size is only re-calculated once Text, Width, Height or available space changes. I looked around with Reflector but things get pretty complex down there so I couldn't find the source for this.
Here is a small workaround that listens to changes in BorderThickness and in the changed event handler, make a small change to the Width and once it is updated, change it right back
public class MyTextBox : TextBox
{
public MyTextBox()
{
DependencyPropertyDescriptor borderThickness
= DependencyPropertyDescriptor.FromProperty(MyTextBox.BorderThicknessProperty, typeof(MyTextBox));
borderThickness.AddValueChanged(this, OnBorderThicknessChanged);
}
void OnBorderThicknessChanged(object sender, EventArgs e)
{
double width = this.Width;
SizeChangedEventHandler eventHandler = null;
eventHandler = new SizeChangedEventHandler(delegate
{
this.Width = width;
this.SizeChanged -= eventHandler;
});
this.SizeChanged += eventHandler;
this.Width = this.ActualWidth + 0.00000001;
}
}
First of all, this looks like a bug.
If the problem is that dynamic changes of the border thickness are not taken into account, you can perhaps make a workaround by creating a dependency property with AffectsMeasure in FrameworkPropertyMetadata, and bind it to the border thickness. Hope this quirk helps.
If the static setting of the border thickness are not taken into account, you can try to replace the TextBox's default template with your own (correct) version.

How to get FrameworkElement properties before its unloading

It is need to realize UI settings system - loading/saving some properites of UI elements (which can be modified by user in runtime) from/into persistent storage. For example:
DevExpress grid control - columns width's, visibility, summary area and so on (this control has set of methods pairs like RestoreLayoutFrom/SaveLayoutTo, but SaveLayoutToStream(Xml) doesnt work in grid.Unloaded handler - when grid is disconnected from PresentationSource)
grid rows/columns, which widths/heights can be adjusted by user via GridSplitter
sizeable popup controls
It is easy to set up controls properties from settings storage after controls loading/initializing/etc, but how catch the moment before controls unloading (when they still remains in visual tree) in order to retrieve their settings for saving?
Short description
I intend to create singleton - UISettingsManager, which inside has a Dictionary with pairs of [element Uid, element settings data]. In visual container (Window, UserControl) this manager can be used in a way like this:
public partial class PageHeader : UserControl
{
public PageHeader()
{
InitializeComponent();
UISettingsManager.RestoreSettings(myGridControl);
UISettingsManager.RestoreSettings(myPopup);
}
}
myGridControl & myPopup has unique Uid's (in application scope), so UISettingsManager can retrieve their settings from inner dictionary & apply it to the controls; of course UISettingsManager knows, how to work with some different types of controls.
But when it is the right moment to store settings of controls, which container is Window or UserControl?
I would use the Window's Closing event.
public class MyWindow : Window
{
public MyWindow()
{
this.Closing += new System.ComponentModel.CancelEventHandler(MyWindow_Closing);
}
void MyWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
// Save what I want to here
}
}
This would be the safest bet, because you will always, at some point, close the window.
However, there may be alternatives, including the Unloaded event for a user control.

How can I animate a property dynamically in a Silverlight 4 UserControl?

I've run into a puzzling limitation in a Silverlight 4 UserControl.
What I'm trying to achieve is to have a panel, which slides out from a minimised state when a button is pressed, but the title bar of it should be draggable with which this maximised state can be resized.
What I've done for the sliding out is to animate the MaxHeight property of the parent Grid of this panel which works quite well even with no hardcoded Height for the panel, but I don't know how can I make this dynamic.
Trying to bind a variable from the code-behind to the 'To' parameter of the 'DoubleAnimation' didn't work, it just silently gets ignored.
As I'm creating UserControls to represent Views, the elements with x:Name properties won't get autogenerated.
I tried to work around this using the code below which mimics what happens in the autogenerated code (with the added bonus of only being done after the layout is actually loaded):
public DoubleAnimation PanelOpenMaxHeightDoubleAnimation;
private void LayoutRoot_Loaded(object sender, System.Windows.RoutedEventArgs e)
{
var LayoutRootreference = sender as Grid;
PanelOpenMaxHeightDoubleAnimation = ((DoubleAnimation)(LayoutRootreference.FindName("PanelOpenMaxHeightDoubleAnimation")));
PanelOpenMaxHeightDoubleAnimation.To = 383;
}
This however breaks when trying to set the value of To, as FindName returns null (I have x:Name manually set in XAML for this particular animation to "PanelOpenMaxHeightDoubleAnimation"). I have the sneaking suspicion FindName can't pick DoubleAnimations up from VisualStates, only actual layout children?
I did find the documentation about XAML Namescopes at http://msdn.microsoft.com/en-us/library/cc189026(v=VS.95).aspx#UserControls, but didn't really understand what my options are from this paragraph (other than being very limited):
For the case of a UserControl, there is no equivalent template part attribute convention for parts of the UserControl in the definition XAML, nor is there a template applied at all. Nevertheless, the namescopes between definition and usage remain disconnected, because the definition namescope is defined and then effectively sealed when you package your UserControl into an assembly for reuse. A best practice here is to define your UserControl such that any value that needs to be set to modify the definition XAML is also exposed as a public property of the UserControl.
What does it mean by the last sentence?
Wondering can I do next? Should I try to generate the entire state from code?
Well, managed to work it out so I'm sharing the solution.
Instead of trying to get a reference to the DoubleAnimation in Resources, I named the Grid in the layout I want to animate and get a reference to that using the code in the original question:
var SlidePanel = ((Grid)(LayoutRootreference.FindName("SlidePanel")));
This does return the element and using that it's possible to create a DoubleAnimation and a Storyboard from scratch purely in code. I just used this code example as a starting point: http://msdn.microsoft.com/en-us/library/cc189069(VS.95).aspx#procedural_code
Best part is, you can change the DoubleAnimation.To parameter even after setting everything up in the Storyboard, so now what I'm doing is just resetting that to my calculated value every time before calling Storyboard.Begin().
It's a bit fiddly to set all these up manually, but at least it works nicely once you do.

Resources