Change children of StackPanel when StackPanel parent is resizing - c#

I have some StackPanels in a Grid. They are filled with Labels (height of all labels > displayable space). A possible XAML would be:
<Grid>
<StackPanel>
<Label Content="bla" Background="lime" />
<Label ...>
...
</StackPanel>
</Grid>
Every time the size of the Grid changes, I need to change the inner content of the StackPanel. I need to hide the overflowing Label that is only shown partly. To achieve this, I can use following solutions: with a Converter and make a new class that is inherited from StackPanel.
I want to create a different way by using an attached property. I have following code:
//DepProp OverflowVisibility (double), can save height value
public static void Initialized(DependencyObject pObject, DependencyPropertyChangedEventArgs e)
{
var panel = (pObject as Panel) //the StackPanel in this case
panel.SizeChanged += panel_updateInnerLayout;
}
static void panel_LayoutUpdated(object sender, EventArgs e)
{
var parent = sender as Panel;
if(parent != null)
foreach(FrameworkElement element in parent.Children)
{
var elementPos = element.TransformToAncestor(parent).Transform(new Point(0,0));
if(element.ActualHeight + elementPos.Y >
(double)parent.GetValue(OverflowVisibilityProperty))
element.Visibility = Visibility.Hidden;
else
element.Visibility = Visibility.Visible;
}
}
And an example for usage in XAML:
<Grid>
<ItemsControl>
<ItemsPanel>
<ItemsPanelTemplate>
<StackPanel own:OverflowVisibility.OverflowVisibility="{Binding Grid height}" />
</ItemsPanelTemplate>
</ItemsPanel>
</ItemsControl>
</Grid>
Every time the StackPanel changes it's size, I can update my labels with the panel_updateInnerLayout event handler. When the size of StackPanel is changed, everything is working fine.
My Problem: The StackPanel itself doesn't raise SizeChanged because it has a bigger height than the Grid. I need an event that raises every time the Grid changes its height.
My Question: Is there any event instead of SizeChanged, that is called every time I change the Grid size? If not, is there an alternative way with an attached property to solve my problem?
I also tried to set a binding of the height of the StackPanel to ItemsControl ActualHeight but it does not raise the SizeChanged.

Unfortunately, you have chosen the wrong Panel control for your requirements. The StackPanel should only be used when the resizing of its contents is not required, because it doesn't provide any child control resizing abilities. Therefore, you should use a Grid, or any other Panel that provides resizing abilities. Please refer to the Panels Overview page on MSDN for further help with choosing the appropriate Panel.

There are 2 possible events: SizeChanged and LayoutUpdated. SizeChanged doesn't work because the StackPanel is not resized. LayoutUpdated does not work, because the sender of this event is always null and i can not locate what StackPanel was the source.
For more see: Layout Events - SizeChanged and LayoutUpdated.
So there are no possible event available for my approach.
While I searched for a solution, I also found out, that I used the term attached property also for attached behavior and blend behavior. The difference is described here. Summary: properties do nothing with the object but their presence can be used, behaviours change the object behavior. Blend behaviors are behaviors that Microsoft created for Microsoft Blend. The code in my question is attached behavior.
For attached behaviors I didn't found a solution, but for Blend behaviors i found some. You need to add a reference Micosoft.Windows.Interactivity.
After that i used this code:
public class OverflowVisibilityBehavior : Behavior<VirtualizingStackPanel>
{
protected override void OnAttached()
{
AssociatedObject.LayoutUpdated += AssociatedObject_LayoutUpdated;
base.OnAttached();
}
protected override void OnDetaching()
{
AssociatedObject.LayoutUpdated -= AssociatedObject_LayoutUpdated;
}
void AssociatedObject_LayoutUpdated(object sender, EventArgs e)
{
var parent = AssociatedObject; //that solves the problem: you can get a "sender" information
//...
// Instead of property GetValue
(VisualTreeHelper.GetParent(parent) as ItemsPresenter).ActualHeight
}
}
Some problems left: The parent of the StackPanel must raise SizeChanged. If you put a StackPanel in a StackPanel in a Grid, you need modify it.
Second: LayoutUpdated changes the StackPanel several times. More times than i need.

Related

UWP GridView Image Stretch bug

I have a GridView control bound to a collection of objects having a BitmapImage property. GridView item template's Image control has a fixed size, while actual pictures may vary in size, can be smaller or bigger than the Image. So i use Stretch=Uniform for bigger pictures, and Stretch=None for smaller. I set the Stretch property on Image_Loaded event:
private void img_ImageOpened(object sender, RoutedEventArgs e)
{
var img = sender as Image;
if (img.Width > (img.Source as BitmapImage).PixelWidth)
{
img.Stretch = Stretch.None;
}
else
{
img.Stretch = Stretch.Uniform;
}
}
So the pics are fitted pretty well:
But if i clear the bound collection and fill it again, things get really messy:
I have spent pretty much time trying to resolve this issue. Image_Loaded isn't called for the second time, so i thought it's something with item caching. I have tried to set CacheMode to null, but that didn't help. Tried to handle various events but with no success either.
Please help!
Thanks
Download my project - i have removed anything not related to the problem, there are only 90 lines of code.
PS i have found the right event to subscribe, it's Image_DataContextChanges. It seems GridView items are reused, and on updates objects and particular grid items can be confused. Image_Loaded isn't called, so an object gets into a random grid item with arbitrary stretching. DataContextChanges fires each time instead so it can be used to change stretching method on the fly.
And while it works i think the Clemens' solution below is just better. Will use it next time.
Instead of adjusting the Image's Stretch property in code behind, you can put the Image control in a Viewbox, which in addition to Stretch also has a StretchDirection property. If you set that to DownOnly, the images will only be stretched to smaller sizes.
<DataTemplate x:DataType="local:Thing">
<Border BorderBrush="Black" BorderThickness="1" Background="Black">
<Viewbox Width="48" Height="48" Stretch="Uniform" StretchDirection="DownOnly">
<Image Stretch="None" Source="{x:Bind Image}" />
</Viewbox>
</Border>
</DataTemplate>
I fixed your code by making sure that you clean up the Stuff collection entirely when hiding the grid
Stuff = null
This means that in the Show_Click handler, you reinitialize the collection.
Stuff = new ObservableCollection<Thing>();
If you will continue to use bindings, you'll need to raise a PropertyChanged notification when you do that (recommended). If you don't want to use bindings, just re-set the ItemsSource to the new instance of Stuff (see below).
private void Show_Click(object sender, RoutedEventArgs e)
{
Stuff = new ObservableCollection<Thing>();
foreach (var s in source.Except(Stuff)) Stuff.Add(s);
gv.ItemsSource = Stuff;
}
private void Hide_Click(object sender, RoutedEventArgs e)
{
Stuff.Clear();
Stuff = null;
}

UWP MapControl, how to handle map element select and unselect events?

In my UWP app I want user to allow select map element (to see detailed info) and unselect the map element by clicking empty are of the map.
In MapControl there are two events which I try to use:
MapElementClick(MapControl sender, MapElementClickEventArgs args)
This event is fired when user clicks map element, I can handle map element selection here.
MapTapped(MapControl sender, MapInputEventArgs args)
This event is fired when user tap's map. I could handle map element unselection here but the problem is that this event is also fired when map element is tapped.
How I should handle this map element unselection? I think there is no way how I can cancel click/tap event bubble from MapElementClick() to prevent MapTapped() firing?
This is my first answer here and I'm also kind of a C# newbie so be gentle pls :D
I had the same problem, and this is the workaround i did: I created two grids over my MapControl, one with margin, this is where you can show the detailed information, and an outer grid, which will detect the tap outside the information grid:
<Grid>
<maps:MapControl
x:Name="MapControl"
MapElementClick="MapControl_MapElementClick"
MapServiceToken="YourMapToken">
<Grid Name="OverlayGrid"
Background="Transparent"
Visibility="Collapsed"
Tapped="OverlayGrid_Tapped"/>
<Grid Name="InfoGrid"
Margin="50,50,50,50"
BorderBrush="Firebrick"
BorderThickness="2"
Background="White"
Visibility="Collapsed"/>
</Grid>
In the codebehind:
private void MapControl_MapElementClick(MapControl sender, MapElementClickEventArgs args)
{
InfoGrid.Visibility = Visibility.Visible;
OverlayGrid.Visibility = Visibility.Visible;
}
private void OverlayGrid_Tapped(object sender, TappedRoutedEventArgs e)
{
InfoGrid.Visibility = Visibility.Collapsed;
OverlayGrid.Visibility = Visibility.Collapsed;
}
This way whatever MapElement you click on will show you the grid where you can show your detailed information for the specific Mapelement, and if you click outside, both will be hidden. Of course you can use any other layout control for showing the information you want.

How to receive focus when clicking on the background of a stackpanel?

I have a UserControl which contains a Label and a TextBox. Both are placed inside a stackpanel which is placed in a border.
I now want to receive an Event when the mouse clicks somewere inside the stackpanel or the border. I tried several things, as using transparent Backgrounds, different Events like ismousedirectlyover etc.
Is there a way i can solve this?
You can try by capturing the mousedown event inside your textbox, your label and your stackpanel and bind them all directly to the same method, you will allways get the mousedown event independent on where you clicked.
You can also try to set the
Panel.Zindex
property to a higher number in the stackpanel and then only capture the mousedown event on it.
1) Add an handler for the MouseLeftButtonDown to the border:
<Border MouseLeftButtonDown="Border_MouseLeftButtonDown">
<StackPanel Background="Transparent">
<TextBox x:Name="Text" />
</StackPanel>
</Border>
2) Set the focus manually to the TextBox:
private void Border_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Text.Focus();
}
make sure to set the background of the stackpanel to transparent.

WinRT Controls Over Scrollviewer Consuming Scrolling

My issue is that I have a few controls (buttons, combo-boxes, hit test visible controls, etc) that are on top of a scrollviewer. Now there is no reason for these controls to consume a pointer wheel changed event, and in checking so they do not. But it seems that when the pointer is over these controls and I attempt to scroll, the scrolling event does not get fired on the scrollviewer (I believe that actual event that is supposed to fire is ViewChanged). Now the buttons and stuff should still handle their regular events, such as PointerPressed, KeyDown, etc. But I want to stop them from consuming the event that would cause the scrollviewer to scroll. Any ideas? Thanks in advance.
This is a quick example of what I'm dealing with:
<Grid>
<ScrollViewer>
<StackPanel>
<!-- Insert any number of things here -->
</StackPanel>
</ScrollViewer>
<Button>Hello World</Button>
</Grid>
Add an event to the controls:
public void UIElement_PointerWheelChanged(object sender, PointerWheelChangedEventArgs e)
{
e.Handled = false;
}

Modifying a property at design-time doesn't update XAML in Expression Blend 4

I've been working on a custom panel for WPF and have run into a problem with some design-time code. To boil the issue down, if I have some code running at design time, and that code modifies a property on some object (either the panel or a child of the panel), in the designer, I see the appropriate change, but the XAML that makes up the window, nothing updates.
example:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows;
namespace StackOverflowExample
{
class MyPanel : Canvas
{
public MyPanel()
{
this.SizeChanged += new System.Windows.SizeChangedEventHandler(MyPanel_SizeChanged);
}
void MyPanel_SizeChanged(object sender, System.Windows.SizeChangedEventArgs e)
{
foreach (FrameworkElement child in this.Children)
{
if (child != null)
{
double widthDelta = e.NewSize.Width - e.PreviousSize.Width;
double newWidth = Math.Max(0.0, child.Width + widthDelta);
child.Width = newWidth;
}
}
}
}
}
If you create a new WPF application and drop that class in, then create a window with this XAML:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:StackOverflowExample" x:Class="StackOverflowExample.MainWindow"
Title="MainWindow" Height="350" Width="525">
<Canvas>
<local:MyPanel Height="153" Canvas.Left="108" Canvas.Top="43" Width="278">
<Rectangle Fill="#FFF4F4F5" Height="77" Canvas.Left="43" Stroke="Black" Canvas.Top="37" Width="87"/>
</local:MyPanel>
</Canvas>
</Window>
If you then change the width of the panel (either by dragging the width handle in the designer, or changing the width property in the property editor), the rectangle inside will update correctly in the design surface. However, if you look at the XAML, only the width of the panel updates, the width of the rectangle remains unchanged. If you build the project after changing the width of the panel, the rectangle goes back to the value defined in the XAML :(
So I spent a good amount of time scouring the internet for solutions to this, and from what I've gathered, this is because the designers have the notion of the Source (XAML), the the View (the actual thing on the designer you're playing with) and the Instance (the in-memory instantiation of the object, defined by the ModelItem class). It's the Instance that needs to be modified to have the XAML update from what I gather. However, I've had no luck in getting ahold of the ModelInstance.
This Post Led me down the direction to try something like this:
...
if (child != null)
{
double widthDelta = e.NewSize.Width - e.PreviousSize.Width;
double newWidth = Math.Max(0.0, child.Width + widthDelta);
EditingContext ec = new EditingContext();
ModelTreeManager mtm = new ModelTreeManager(ec);
System.Activities.Presentation.Model.ModelItem model = mtm.CreateModelItem(null, child);
model.Properties["Width"].SetValue(newWidth);
}
However, this just manages to crash Blend... I stepped though this code in the debugger, and it tells me the crash is a nullRefferenceException, but it's unclear what's null. I believe it's the root property of the modelTreeManager, but if so, I have no idea how to set that correctly.
So what I'm trying to accomplish seems simple enough. Change a property of something at design-time and have the change serialize out to XAML... Supposedly this is handled via modifying the ModelItem backer of the item on the designer, however, I can't find any documentation on how to accomplish this.
Further reading
I realize my example is changing layout, and there are other ways to accomplish this (such as using the layout system (arrangeOverride and measureOverride), however this approach does not give me the type of control I need.
Also, I barked up the adorner tree, creating a custom adorner that had no UI, but hooked the panel's onPropertyChanged event and modified the children's width. This ACTUALLY WORKS, since the adorner can get ahold of the ModelItem, but it only works in limited fashion. With the adorner route, the method works PERFECTLY in Cider (visual studio 2010), but only half-works in Blend. In blend, if the width of the panel is changed in the property editor, the children get updated. However, if the width-handle on the design surface is dragged in blend, the children DO NOT get updated (in visual studio, the children do get updated when the design-surface handle is used).
The code for the adorner is much more complicated, but follows Walkthrough: Creating a Design-time Adorner (search MSDN, I can only post one link) modified to work for both designers (using MyAssembly.Design.dll instead of MyAssembly.VisualStuido.Design.dll). It might be worth noting that the opacity slider in this updates in real-time (as it's dragged) in Cider, but only updates on mouse release in Blend. Here is the code for my adorner if anyone's interested.
class MyPanelAdornerProvider : PrimarySelectionAdornerProvider
{
private ModelItem adornedControlModel;
private double previousWidth;
protected override void Activate(Microsoft.Windows.Design.Model.ModelItem item)
{
adornedControlModel = item;
adornedControlModel.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(AdornedControlModel_PropertyChanged);
previousWidth = (double)adornedControlModel.Properties["Width"].ComputedValue;
base.Activate(item);
}
protected override void Deactivate()
{
adornedControlModel.PropertyChanged -= new System.ComponentModel.PropertyChangedEventHandler(AdornedControlModel_PropertyChanged);
base.Deactivate();
}
void AdornedControlModel_PropertyChanged( object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == "Width")
{
double widthDelta = ((double)adornedControlModel.Properties["Width"].ComputedValue) - previousWidth;
ModelItemCollection children = adornedControlModel.Properties["Children"].Collection;
foreach (ModelItem item in children)
{
item.Properties["Width"].SetValue(Math.Max(0.0, ((double)item.Properties["Width"].ComputedValue) + widthDelta));
}
previousWidth = (double)adornedControlModel.Properties["Width"].ComputedValue;
}
}
}

Categories

Resources