May I modify my panel's children's width? - wpf

I imagine it for example
public int ChildSize
{
get { return childSize; }
set
{
foreach (UIElement child in Children)
{
child.DesiredSize.Width = value;
}
}
}

I suspect you are writing a custom panel, which is quite an advanced task.
Your Panel should create overrides for the MeasureOverride and ArrangeOverride methods.
In these methods you should be calling each child's Measure and Arrange methods passing in appropriate Size and Rect according your determined childSize and calculated relative positions of each child.

Related

ItemContainerGenerator.ContainerFromItem() returns null?

I'm having a bit of weird behavior that I can't seem to work out. When I iterate through the items in my ListBox.ItemsSource property, I can't seem to get the container? I'm expecting to see a ListBoxItem returned, but I only get null.
Any ideas?
Here's the bit of code I'm using:
this.lstResults.ItemsSource.ForEach(t =>
{
ListBoxItem lbi = this.lstResults.ItemContainerGenerator.ContainerFromItem(t) as ListBoxItem;
if (lbi != null)
{
this.AddToolTip(lbi);
}
});
The ItemsSource is currently set to a Dictionary and does contain a number of KVPs.
I found something that worked better for my case in this StackOverflow question:
Get row in datagrid
By putting in UpdateLayout and a ScrollIntoView calls before calling ContainerFromItem or ContainerFromIndex, you cause that part of the DataGrid to be realized which makes it possible for it return a value for ContainerFromItem/ContainerFromIndex:
dataGrid.UpdateLayout();
dataGrid.ScrollIntoView(dataGrid.Items[index]);
var row = (DataGridRow)dataGrid.ItemContainerGenerator.ContainerFromIndex(index);
If you don't want the current location in the DataGrid to change, this probably isn't a good solution for you but if that's OK, it works without having to turn off virtualizing.
Finally sorted out the problem... By adding VirtualizingStackPanel.IsVirtualizing="False" into my XAML, everything now works as expected.
On the downside, I miss out on all the performance benefitst of the virtualization, so I changed my load routing to async and added a "spinner" into my listbox while it loads...
object viewItem = list.ItemContainerGenerator.ContainerFromItem(item);
if (viewItem == null)
{
list.UpdateLayout();
viewItem = list.ItemContainerGenerator.ContainerFromItem(item);
Debug.Assert(viewItem != null, "list.ItemContainerGenerator.ContainerFromItem(item) is null, even after UpdateLayout");
}
Step through the code with the debugger and see if there is actually nothing retured or if the as-cast is just wrong and thus turns it to null (you could just use a normal cast to get a proper exception).
One problem that frequently occurs is that when an ItemsControl is virtualizing for most of the items no container will exist at any point in time.
Also i would not recommend dealing with the item containers directly but rather binding properties and subscribing to events (via the ItemsControl.ItemContainerStyle).
Use this subscription:
TheListBox.ItemContainerGenerator.StatusChanged += (sender, e) =>
{
TheListBox.Dispatcher.Invoke(() =>
{
var TheOne = TheListBox.ItemContainerGenerator.ContainerFromIndex(0);
if (TheOne != null)
// Use The One
});
};
I'm a bit late for the party but here's another solution that's fail-proof in my case,
After trying many solutions suggesting to add IsExpanded and IsSelected to underlying object and binding to them in TreeViewItem style, while this mostly works in some case it still fails ...
Note: my objective was to write a mini/custom Explorer-like view where when I click a folder in the right pane it gets selected on the TreeView, just like in Explorer.
private void ListViewItem_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
var item = sender as ListViewItem;
var node = item?.Content as DirectoryNode;
if (node == null) return;
var nodes = (IEnumerable<DirectoryNode>)TreeView.ItemsSource;
if (nodes == null) return;
var queue = new Stack<Node>();
queue.Push(node);
var parent = node.Parent;
while (parent != null)
{
queue.Push(parent);
parent = parent.Parent;
}
var generator = TreeView.ItemContainerGenerator;
while (queue.Count > 0)
{
var dequeue = queue.Pop();
TreeView.UpdateLayout();
var treeViewItem = (TreeViewItem)generator.ContainerFromItem(dequeue);
if (queue.Count > 0) treeViewItem.IsExpanded = true;
else treeViewItem.IsSelected = true;
generator = treeViewItem.ItemContainerGenerator;
}
}
Multiple tricks used in here:
a stack for expanding every item from top to bottom
ensure to use current level generator to find the item (really important)
the fact that generator for top-level items never return null
So far it works very well,
no need to pollute your types with new properties
no need to disable virtualization at all.
Although disabling virtualization from XAML works, I think it's better to disable it from the .cs file which uses ContainerFromItem
VirtualizingStackPanel.SetIsVirtualizing(listBox, false);
That way, you reduce the coupling between the XAML and the code; so you avoid the risk of someone breaking the code by touching the XAML.
Most probably this is a virtualization-related issue so ListBoxItem containers get generated only for currently visible items (see https://msdn.microsoft.com/en-us/library/system.windows.controls.virtualizingstackpanel(v=vs.110).aspx#Anchor_9)
If you are using ListBox I'd suggest switching to ListView instead - it inherits from ListBoxand it supports ScrollIntoView() method which you can utilize to control virtualization;
targetListView.ScrollIntoView(itemVM);
DoEvents();
ListViewItem itemContainer = targetListView.ItemContainerGenerator.ContainerFromItem(itemVM) as ListViewItem;
(the example above also utilizes the DoEvents() static method explained in more detail here; WPF how to wait for binding update to occur before processing more code?)
There are a few other minor differences between the ListBox and ListView controls (What is The difference between ListBox and ListView) - which should not essentially affect your use case.
VirtualizingStackPanel.IsVirtualizing="False" Makes the control fuzzy . See the below implementation. Which helps me to avoid the same issue.
Set your application VirtualizingStackPanel.IsVirtualizing="True" always.
See the link for detailed info
/// <summary>
/// Recursively search for an item in this subtree.
/// </summary>
/// <param name="container">
/// The parent ItemsControl. This can be a TreeView or a TreeViewItem.
/// </param>
/// <param name="item">
/// The item to search for.
/// </param>
/// <returns>
/// The TreeViewItem that contains the specified item.
/// </returns>
private TreeViewItem GetTreeViewItem(ItemsControl container, object item)
{
if (container != null)
{
if (container.DataContext == item)
{
return container as TreeViewItem;
}
// Expand the current container
if (container is TreeViewItem && !((TreeViewItem)container).IsExpanded)
{
container.SetValue(TreeViewItem.IsExpandedProperty, true);
}
// Try to generate the ItemsPresenter and the ItemsPanel.
// by calling ApplyTemplate. Note that in the
// virtualizing case even if the item is marked
// expanded we still need to do this step in order to
// regenerate the visuals because they may have been virtualized away.
container.ApplyTemplate();
ItemsPresenter itemsPresenter =
(ItemsPresenter)container.Template.FindName("ItemsHost", container);
if (itemsPresenter != null)
{
itemsPresenter.ApplyTemplate();
}
else
{
// The Tree template has not named the ItemsPresenter,
// so walk the descendents and find the child.
itemsPresenter = FindVisualChild<ItemsPresenter>(container);
if (itemsPresenter == null)
{
container.UpdateLayout();
itemsPresenter = FindVisualChild<ItemsPresenter>(container);
}
}
Panel itemsHostPanel = (Panel)VisualTreeHelper.GetChild(itemsPresenter, 0);
// Ensure that the generator for this panel has been created.
UIElementCollection children = itemsHostPanel.Children;
MyVirtualizingStackPanel virtualizingPanel =
itemsHostPanel as MyVirtualizingStackPanel;
for (int i = 0, count = container.Items.Count; i < count; i++)
{
TreeViewItem subContainer;
if (virtualizingPanel != null)
{
// Bring the item into view so
// that the container will be generated.
virtualizingPanel.BringIntoView(i);
subContainer =
(TreeViewItem)container.ItemContainerGenerator.
ContainerFromIndex(i);
}
else
{
subContainer =
(TreeViewItem)container.ItemContainerGenerator.
ContainerFromIndex(i);
// Bring the item into view to maintain the
// same behavior as with a virtualizing panel.
subContainer.BringIntoView();
}
if (subContainer != null)
{
// Search the next level for the object.
TreeViewItem resultContainer = GetTreeViewItem(subContainer, item);
if (resultContainer != null)
{
return resultContainer;
}
else
{
// The object is not under this TreeViewItem
// so collapse it.
subContainer.IsExpanded = false;
}
}
}
}
return null;
}
For anyone still having issues with this, I was able to work around this issue by ignoring the first selection changed event and using a thread to basically repeat the call. Here's what I ended up doing:
private int _hackyfix = 0;
private void OnMediaSelectionChanged(object sender, SelectionChangedEventArgs e)
{
//HACKYFIX:Hacky workaround for an api issue
//Microsoft's api for getting item controls for the flipview item fail on the very first media selection change for some reason. Basically we ignore the
//first media selection changed event but spawn a thread to redo the ignored selection changed, hopefully allowing time for whatever is going on
//with the api to get things sorted out so we can call the "ContainerFromItem" function and actually get the control we need I ignore the event twice just in case but I think you can get away with ignoring only the first one.
if (_hackyfix == 0 || _hackyfix == 1)
{
_hackyfix++;
Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
OnMediaSelectionChanged(sender, e);
});
}
//END OF HACKY FIX//Actual code you need to run goes here}
EDIT 10/29/2014: You actually don't even need the thread dispatcher code. You can set whatever you need to null to trigger the first selection changed event and then return out of the event so that future events work as expected.
private int _hackyfix = 0;
private void OnMediaSelectionChanged(object sender, SelectionChangedEventArgs e)
{
//HACKYFIX: Daniel note: Very hacky workaround for an api issue
//Microsoft's api for getting item controls for the flipview item fail on the very first media selection change for some reason. Basically we ignore the
//first media selection changed event but spawn a thread to redo the ignored selection changed, hopefully allowing time for whatever is going on
//with the api to get things sorted out so we can call the "ContainerFromItem" function and actually get the control we need
if (_hackyfix == 0)
{
_hackyfix++;
/*
Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
OnMediaSelectionChanged(sender, e);
});*/
return;
}
//END OF HACKY FIX
//Your selection_changed code here
}

How do you use MeasureOverride in Silverlight?

I think I have understood how MeasureOverrride works, but I am trying to use it in a very simple case and It doesn't work... So now I'm not so sure... After using Measureoverride do I have to use Arrageoverride too or the system will do it for me? The situation is this one: I have a LinearLayout class inherited from Panel and it has two fields called wrapwidht and wrapheigh, if they are true the width or the height of the LinearLayout has to be as its children require. so my Measureoveride looks like:
protected override Size MeasureOverride(Size availableSize) {
Size panelDesiredSize = new Size();
if ((this.widthWrap) || (this.heightWrap))
{
foreach (UIElement elemento in this.Children)
{
System.Diagnostics.Debug.WriteLine(((FrameworkElement)elemento).DesiredSize.ToString());
if (this.Orientation.Equals(System.Windows.Controls.Orientation.Vertical))
{
if (this.widthWrap)
{
//the widest element will determine containers width
if (panelDesiredSize.Width < ((FrameworkElement)elemento).Width)
panelDesiredSize.Width = ((FrameworkElement)elemento).Width;
}
//the height of the Layout is determine by the sum of all the elment that it cointains
if (this.heightWrap)
panelDesiredSize.Height += ((FrameworkElement)elemento).Height;
}
else
{
if (this.heightWrap)
{
//The highest will determine the height of the Layout
if (panelDesiredSize.Height < ((FrameworkElement)elemento).Height)
panelDesiredSize.Height = ((FrameworkElement)elemento).Height;
}
//The width of the container is the sum of all the elements widths
if (this.widthWrap)
panelDesiredSize.Width += ((FrameworkElement)elemento).Width;
}
}
}
System.Diagnostics.Debug.WriteLine("desiredsizzeeeeeee" + panelDesiredSize);
return panelDesiredSize;
}
The children I am aading to the LinerLayout are 3 buttons, but nothing is drawn.. even if the panelDesiredSize filed is correct.. so maybe I didn't understand how it works very well. If anybody can help me would be very nice :-)
Check my answer on a previous post similar to yours: Two Pass Layout system in WPF and Silverlight
The answer is that no, you don't have to override ArrangeOverride, but what is the point of using MeasureOverride if you are not going to use ArrangeOverride?
You should call the Measure method on the child. See the example here: http://msdn.microsoft.com/en-us/library/system.windows.frameworkelement.measureoverride.aspx
You must call Measure on "elemento". It's during the Measure that Silverlight creates the UI elements declared in the elemento's template since they'll be needed to actually measure and come up with a desired size. You should then use the elemento.DesiredSize.Width and Height to come up with the desired size for your panelDesiredSize.

Restoring exact scroll position of a listbox in Windows Phone 7

I'm working on making an app come back nicely from being tombstoned. The app contains large listboxes, so I'd ideally like to scroll back to wherever the user was while they were scrolling around those listboxes.
It's easy to jump back to a particular SelectedItem - unfortunately for me, my app never needs the user to actually select an item, they're just scrolling through them. What I really want is some sort of MyListbox.ScrollPositionY but it doesn't seem to exist.
Any ideas?
Chris
You need to get hold of the ScrollViewer that is used by the ListBox internally so you can grab the value of the VerticalOffset property and subsequently call the SetVerticalOffset method.
This requires that you reach down from the ListBox through the Visual tree that makes up its internals.
I use this handy extension class which you should add to your project (I've gotta put this up on a blog because I keep repeating it):-
public static class VisualTreeEnumeration
{
public static IEnumerable<DependencyObject> Descendents(this DependencyObject root, int depth)
{
int count = VisualTreeHelper.GetChildrenCount(root);
for (int i = 0; i < count; i++)
{
var child = VisualTreeHelper.GetChild(root, i);
yield return child;
if (depth > 0)
{
foreach (var descendent in Descendents(child, --depth))
yield return descendent;
}
}
}
public static IEnumerable<DependencyObject> Descendents(this DependencyObject root)
{
return Descendents(root, Int32.MaxValue);
}
public static IEnumerable<DependencyObject> Ancestors(this DependencyObject root)
{
DependencyObject current = VisualTreeHelper.GetParent(root);
while (current != null)
{
yield return current;
current = VisualTreeHelper.GetParent(current);
}
}
}
With this available the ListBox (and all other UIElements for that matter) gets a couple of new extension methods Descendents and Ancestors. We can combine those with Linq to search for stuff. In this case you could use:-
ScrollViewer sv = SomeListBox.Descendents().OfType<ScrollViewer>().FirstOrDefault();

Custom Right Aligned StackPanel control layout in Silverlight

I'm trying to create a custom layout container, with the same characteristics of StackPanel, with the exception that it lays out the items starting at the right edge of the screen. Needless to say it does not work correctly.
I have identified a flaw inside ArrangeOverride() where the line
Point elementOrigin = new Point(this.DesiredSize.Width, 0);
simply creates a point # 0, 0. In other words this.DesiredSize.Width = 0. I understand that the measuring step happens before the arranging step, so I would expect this control will have the DesiredSize property set. How could I start rendering from the right side of the screen otherwise? Is it even possible?
Secondly the finalSize argument that is passed in to the function is much much larger than the area required by the three buttons I have defined in the test xaml. Something to the tune of 1676 by 909 vs a required 250 by 60 or so.
Thank you.
Here's my code:
protected override Size MeasureOverride(Size availableSize)
{
Size availableSpace = new Size(double.PositiveInfinity, double.PositiveInfinity);
Size desiredSize = new Size(0, 0);
foreach (UIElement child in this.Children)
{
child.Measure(availableSpace);
desiredSize.Width += child.DesiredSize.Width;
desiredSize.Height = Math.Max(desiredSize.Height, child.DesiredSize.Height);
}
return base.MeasureOverride(desiredSize);
}
protected override Size ArrangeOverride(Size finalSize)
{
Point elementOrigin = new Point(this.DesiredSize.Width, 0);
foreach (UIElement child in this.Children)
{
Rect childBounds = new Rect(elementOrigin, child.DesiredSize);
elementOrigin.X -= child.DesiredSize.Width;
child.Arrange(childBounds);
}
return base.ArrangeOverride(finalSize);
}
You need to simply return your desiredSize from your MeasureOverride implementation, you don't want to be calling the base version of this method, you are replacing the default implementation with yours.
Similarly with ArrangeOverride you are providing the implementation, you are replacing the default implementation so don't call the base version of this method. Simply return finalSize.

How do I bring an item to the front in wpf?

I simply have two grid on top of one another. Given one state of the world, I want grid A to be on top, given another state of the world, I want grid B to be on top. In the old days we could just call grid.BringToFront(), but that doesn't exist anymore, and I can't figure out any way to make that happen.
The best I can figure, I need to create my own custom classes to allow this functionality, but that seems like major overkill for something that used to be so simple.
You can use the Panel.ZIndex property to change the display order of elements in a panel
You have to use the Z index property, and because there are no built-in function to do what you want, I made my own.
The higher the Z value, the 'closer' to front the control is.
So you want to put your control on top without having to set an arbitrary high Z value.
So here is a small function I wrote for myself to do exactly that.
Note: this assume that you are using a Canvas and UserControls.
So you might need to adapt it a little bit if that's not your case.
Basically it will get the index of the control to move, then any control currently above it will go down by 1 and the control to move will be put on top (to maintain hierarchy).
static public void BringToFront(Canvas pParent, UserControl pToMove)
{
try
{
int currentIndex = Canvas.GetZIndex(pToMove);
int zIndex = 0;
int maxZ = 0;
UserControl child;
for (int i = 0; i < pParent.Children.Count; i++)
{
if (pParent.Children[i] is UserControl &&
pParent.Children[i] != pToMove)
{
child = pParent.Children[i] as UserControl;
zIndex = Canvas.GetZIndex(child);
maxZ = Math.Max(maxZ, zIndex);
if (zIndex > currentIndex)
{
Canvas.SetZIndex(child, zIndex - 1);
}
}
}
Canvas.SetZIndex(pToMove, maxZ);
}
catch (Exception ex)
{
}
}
To whom it may concern:
ZIndex property is 0 by default, so if you have (like me) a Canvas with more than 1 element (>4000 Shapes in my case), all will have ZIndex = 0, so changing the ZIndexes with this method will have no effect.
For this to work, I set the ZIndexes to a known value after creating all the elements, so they can be ordered after.
int zIndex = 0;
for (int i = 0; i < canvas.Children.Count; i++) {
UIElement child = canvas.Children[i] as UIElement;
if (canvas.Children[i] is UIElement) Canvas.SetZIndex(child, zIndex++);
}
Instead of stacking the two grids, change the visibility properties so the grid you aren't using is collapsed.
Expanding on the answer from #PicMickael, this will do exactly as they described but with less instructions:
public void BringToFront<T>(T uiElement, Canvas canvas)
{
try
{
foreach (UIElement s in canvas.Children)
{
Canvas.SetZIndex(s, 1);
}
Canvas.SetZIndex(uiElement as UIElement, 2);
}
catch (Exception ex)
{
WriteLog.Error(ex);
}
}

Resources