WPF GridSplitter gets jammed when ListView gets focus - wpf

The GridSplitters in my WPF app are behaving strangely. My application has a 3-panel layout, implemented as a grid with 5 columns, where 0/2/4 have content and 1/3 have GridSplitters. The middle panel is a Grid with a ListView.
Everything works fine until the ListView gets focus, at which point the splitters mostly stop moving. You can drag them a pixel or two, then they freeze. The app sees an initial motion event, but nothing further. You can repeat this a few times to move the splitter multiple pixels, but if you try to move another splitter the previous one jumps back where it was. There are two more splitters in the side panels that continue to work normally.
Reloading the project causes the ListView to lose focus, and the splitters start working again.
That's probably hard to visualize, so a made a short video.
The XAML is pretty straightforward. The only quirk is that the center panel is defined twice, with mutually-exclusive visibility.
<Grid Name="triptychGrid" DockPanel.Dock="Top">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" MinWidth="100"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*" MinWidth="150"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*" MinWidth="100"/>
</Grid.ColumnDefinitions>
<GridSplitter Grid.Column="1" Width="4" HorizontalAlignment="Left"/>
<GridSplitter Grid.Column="3" Width="4" HorizontalAlignment="Center"/>
<Grid Grid.Column="0" Name="leftPanel"> ... </Grid>
<Grid Grid.Column="2" Name="launchPanel" Visibility="{Binding Path=LaunchPanelVisibility}"> ... </Grid>
<Grid Grid.Column="2" Name="codeListGrid" Visibility="{Binding Path=CodeListVisibility}"> ... </Grid>
<Grid Grid.Column="4" Name="rightPanel"> ... </Grid>
</Grid>
There are event listeners on the panel widths so they get remembered between runs. I restore the widths of the panels when the application first starts, but don't touch them after that.
Everything works just fine until the ListView gets focus. It would be less weird if it froze up entirely. Moving a couple of pixels at a time and then jumping back when other controls are touched seems weird.
I have reproduced this on a desktop running the latest Win10 (.NET reports 10.0.18362) and a virtual machine running the latest Win7. The project is open source.

The problem occurs because you are handling the ItemContainerGenerator.StatusChanged event. When the listView gets resized because of the grid splitter, the even get generated for each pixel or so which mess up the smoothness of the operation.
A quick and lazy fix to that is to cancel that handler when the sliding is in progress, you can achieve that simply by using a boolean, set it to true or false by handling the DragStarted and DragOver of the GridSplitters:
<GridSplitter Name="leftSplitter" Width="4" Grid.Column="1" HorizontalAlignment="Left" DragStarted="LeftSplitter_OnDragStarted" DragOver="LeftSplitter_OnDragOver"/>
<GridSplitter Name="rightSplitter" Width="4" Grid.Column="3" HorizontalAlignment="Center" DragStarted="LeftSplitter_OnDragStarted" DragOver="LeftSplitter_OnDragOver"/>
The handlers look something like that:
private bool _isBeingDraged = false;
private void LeftSplitter_OnDragStarted(object sender, DragStartedEventArgs e)
{
_isBeingDraged = true;
}
private void LeftSplitter_OnDragOver(object sender, DragEventArgs e)
{
_isBeingDraged = false;
}
Then update your ItemContainerGenerator_StatusChanged to consider that boolean:
private void ItemContainerGenerator_StatusChanged(object sender, EventArgs e) {
if(_isBeingDraged)
return;
if (codeListView.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated) {
int index = codeListView.SelectedIndex;
if (index >= 0) {
ListViewItem item =
(ListViewItem)codeListView.ItemContainerGenerator.ContainerFromIndex(index);
if (item != null) {
item.Focus();
}
}
}
}
There might be something more sophisticated to do with the ItemContainerGenerator, but that requires more diggings.

Related

Align center until there isn't enough space WPF

I have a particular layout behavior in mind, and I want to see if anyone can come up with a simple way of doing this before I try to write a custom panel or something. Take a look at this animation I manually put together:
There is an element of fixed width to the far left (the rectangle), and then a second element that is centered- not within the remaining space, but relative to the entire container. As the container shrinks, the centered element stays centered until the left element gets too close. Instead of staying directly in the middle, the "centered" element now stays as close to the center as possible without overlapping. This is the behavior I want.
Using a Grid, I can do something like this:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Border Width="100" Background="Green"/>
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Grid.ColumnSpan="2" FontSize="24" Margin="2, 0">Centered Text</TextBlock>
</Grid>
Which puts everything in the right place, but the TextBlock will start overlapping with the Border instead of moving further right.
So any idea how I can get this to do what I want?
Give this a try:
Name of Window x:Name="window1"
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Border x:Name="border1" Width="100" Background="Green"/>
<TextBlock x:Name="textBlock1" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="24" Grid.ColumnSpan="2">Centered Text</TextBlock>
</Grid>
SizeChanged event
private void Window1_SizeChanged(object sender, SizeChangedEventArgs e) {
if(( ( window1.Width / 2 ) - ( textBlock1.ActualWidth / 2 ) - border1.Width - ( SystemParameters.ResizeFrameVerticalBorderWidth * 2 ) ) <= 0) {
Grid.SetColumn(textBlock1, 1);
textBlock1.HorizontalAlignment = HorizontalAlignment.Left;
} else {
Grid.SetColumn(textBlock1, 0);
textBlock1.HorizontalAlignment = HorizontalAlignment.Center;
}
}

WPF ScrollViewer gives my listview (in a grid row) the whole screen instead of a portion

The main grid on my usercontrol has 3 rows. The top row is a data-bound listvew that takes about 60% of the whole window (there's more data rows than can be displayed and the listview automatically displays a scroll bar - This is good). Second row is a gridsplitter and the 3rd takes up the rest of the display.
This 3rd row has a grid wrapped in a border and also contains a textbox that wraps & can grow larger. When it does, it sometimes pushes the buttons at the very bottom off the screen, so I thought that if I wrapped a ScrollViewer around the main grid that I'd keep my 3 rows on the screen in the same proportion with the existing listview scrollbar left intact and then just get an additional scrollbar on the right that would let me scroll down to see the buttons if the 3rd row grew too tall (like you do on this browser page with scroll bars for the code & the page scroller too.
What happens instead, is that the first row with the listview has expanded vertically to take the whole screen and I can't see rows 2 or 3 at all until I've scrolled to the end of all the items in the listview. I've tried various combinations of hardcoding row heights (bad, I know) 'Auto' & '*' to now avail.
Is there a way to accomplish what I'm trying? I didn't think I'd have to (and down't want to) re-engineer the whole screen for this.
Thanks, I'm new to WPF & it's fun but very frustrating at times!
I'm posting some XAML, below, but I'm not sure it will help.
<ScrollViewer>
<Grid Name="grdEvents" HorizontalAlignment="Center" >
<Grid.RowDefinitions>
<RowDefinition Height="60*" />
<RowDefinition Height="10" />
<RowDefinition Height="30*"/>
</Grid.RowDefinitions>
<ListView SelectionChanged="lvActivities_SelectionChanged" MouseDoubleClick="ListView_MouseDoubleClick" Grid.Row="0" Name="lvActivities" HorizontalAlignment="Stretch" LVLO:ListViewLayoutManager.Enabled="True" >
<!--ItemContainerStyle="{StaticResource SelectedItem}" MouseEnter="lvActivities_MouseEnter" -->
<ListView.ItemContainerStyle>
<Style TargetType="ListBoxItem">
...
</ListView>
<GridSplitter Grid.Row="1" Height="5" HorizontalAlignment="Stretch"> </GridSplitter>
<Border Grid.Row="2" CornerRadius="20"
BorderBrush="Gray"
Background="White"
BorderThickness="2"
Padding="8">
<Grid Name="wpCurrentRow" DataContext="{Binding ElementName=lvActivities, Path=SelectedItem}" Grid.Row="2" Background="{StaticResource ResourceKey=MyBackGroundBrush}">
I don't think you can accomplish what you want with relative row sizes. What you can do, however, is manually set a proportional height to the top row any time the ScrollViewer's size changes. But since you also have a splitter, you will want to stop doing this once the user adjusts the height manually. Take a look at this:
XAML:
<Window x:Class="StackOverflow.Wpf.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<ScrollViewer x:Name="_scrollViewer">
<Grid>
<Grid.RowDefinitions>
<RowDefinition x:Name="_mainRow" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ListView />
<GridSplitter x:Name="_splitter"
Grid.Row="1"
Height="5"
HorizontalAlignment="Stretch"
ResizeDirection="Rows"
ResizeBehavior="PreviousAndNext"
MouseDoubleClick="OnSplitterMouseDoubleClick" />
<Grid Grid.Row="2" />
</Grid>
</ScrollViewer>
</Grid>
</Window>
Code behind:
public partial class MainWindow
{
private bool _shouldUpdateGridLayout;
public MainWindow()
{
InitializeComponent();
EnsureGridLayoutUpdates();
}
private void EnsureGridLayoutUpdates()
{
if (_shouldUpdateGridLayout)
return;
_scrollViewer.SizeChanged += OnScrollViewerSizeChanged;
_splitter.DragCompleted += OnSplitterDragCompleted;
_shouldUpdateGridLayout = true;
}
private void CancelGridLayoutUpdates()
{
if (!_shouldUpdateGridLayout)
return;
_scrollViewer.SizeChanged -= OnScrollViewerSizeChanged;
_splitter.DragCompleted -= OnSplitterDragCompleted;
_shouldUpdateGridLayout = false;
}
private void UpdateGridLayout()
{
_mainRow.Height = new GridLength(2 * _scrollViewer.ActualHeight / 3);
}
private void OnScrollViewerSizeChanged(object s, SizeChangedEventArgs e)
{
UpdateGridLayout();
}
private void OnSplitterDragCompleted(object s, DragCompletedEventArgs e)
{
CancelGridLayoutUpdates();
}
private void OnSplitterMouseDoubleClick(object s, MouseButtonEventArgs e)
{
EnsureGridLayoutUpdates();
UpdateGridLayout();
// Handle the event to prevent DragCompleted from being raised
// in response to the double-click.
e.Handled = true;
}
}
Note that I chose to restore both the default size and automatic size management when the user double-clicks the splitter. It's not the prettiest solution, but it beats using fixed heights. Feel free to encapsulate the behavior in custom panel/control.
Instead of using proportions, use hardcoded heights on the rows. Change the 60* to 60 for the first row.
Also just for the sake of experimentation you could try this:
<Grid.RowDefinitions>
<RowDefinition Height="2*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<ListBox Name="ListBox1" HorizontalAlignment="Left" Height="100" VerticalAlignment="Top" Width="100" Grid.Row="0" Background="Blue"/>
<ScrollViewer Grid.Row="1">
<StackPanel >
<Button Content="Button" HorizontalAlignment="Left" VerticalAlignment="Top" Width="75" Click="Button_Click" />
<TextBox>Hello </TextBox>
</StackPanel>
</ScrollViewer>
It adds a scroll viewer just to the second row and you then use a stack panel to store the rest of the elements. It makes it look a bit better imo too.
The elements were just added for example; replace them with your own.

How to I make an Expander whose expanded size can be controlled via a GridSplitter? [duplicate]

I would like to have something like a resizable Expander. My basic idea was something like this:
<Grid HorizontalAlignment="Left">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="2" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Expander Grid.Column="0" ExpandDirection="Right">
...
</Expander>
<GridSplitter Grid.Column="1" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" />
...
</Grid>
The problem with this: if i move the grid splitter and collaps the expander i got a big empty area. How can make the entire column collapse? Or is there another way to make the expander "resizable"
Not sure what you are trying to accomplish but i think conceptually the Grid should be part of the Expander.Content, would this work for you?
<Expander Header="Test" ExpandDirection="Right" HorizontalAlignment="Left" Background="LightBlue">
<Expander.Content>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="5"/>
</Grid.ColumnDefinitions>
<TextBlock Text="Lorem ipsum dolor sit"/>
<GridSplitter Grid.Column="1" Width="5" ResizeBehavior="PreviousAndCurrent" ResizeDirection="Columns"/>
</Grid>
</Expander.Content>
</Expander>
Edit: Removed all the triggering from the first column as it seemed unnecessary.
Also: For this to work vertically the GridSplitter's HorizontalAlignment must be set to Stretch, otherwise it will have zero width by default (of course everything else that is orientation-specific must be adapted as well but that is straightforward)
HorizontalAlignment is the Microsoft .NET property accessor for what is in reality a dependency property. This particular dependency property quite frequently has its apparent "default" value set differently in subclassed elements, particularly controls. [...] For example, the apparent "default" of HorizontalAlignment for a Label control will be Left, even though Label inherits HorizontalAlignment direct from FrameworkElement. This is because that value was reset within the default style of Label, within the style's control template.
Maybe this will help to solve your "column collapse" problem
XAML:
Add in <Grid> Name="expGrid" and add in <Expander> Collapsed="Expander_Collapsed"
C# Code:
private void Expander_Collapsed(object sender, RoutedEventArgs e)
{
var colomnIndex = Grid.GetColumn(sender as Expander);
var colomn = expGrid.ColumnDefinitions[colomnIndex];
colomn.Width = GridLength.Auto;
}

How to implement Camera app style photo strip on WP7?

I'm running into a number of problems creating an effect very similar to the photo strip in the Camera app.
All I want to do is display a row of grids that each have the same dimensions as the screen (whether in portrait or landscape). Already, I had to do something hacky and create dependency properties that the grids width and height properties bind to to maintain the aspect ratio.
And this works fine. But when I create a StackPanel for my strip and implement my navigation (or just zoom back with the z-index transform) I see that my StackPanel can't display larger than the screen dimensions (it's clipped to the size of just one grid). I thought I found a post describing this issue but I can't find it now - please post if you know which post I'm thinking of or if you know more about this limitation.
The only workaround I've found is to use a ScrollViewer, which is absolutely not the behavior I want, but it allows the StackPanel to be wider than the screen.
My real problem is with the ScrollViewer behavior - because I need to hop from grid to grid (just like the photo strip does) instead of freely scrolling, and as far as I can tell the HorizontalOffset is not an animatable property. I can force it to animate by calling ScrollToHorizontalOffset every 15 milliseconds, basically implementing my own easing effect manually. This seems like a huge hack, and the behavior is very glitchy (either I'm not getting the ManipulationCompleted event every time I expect it - at the end of every swipe action - or the built in inertia physics of ScrollViewer is interfering with my effect).
Does anyone know better workarounds for the issues I've run into, or a completely different way to get the experience of the Camera Photo strip in Silverlight?
I have considered using the Pivot control, but it isn't quite what I want (if I wanted each item to animate out completely before the next one comes in, instead of appearing to be all attached to one strip, there should be less constraining ways to achieve that). More importantly, the strip is just one of many effects I want to be able to do dynamically. I'd like to alternately have a CoolIris-like 3d-tilt, or a FlipPad style page turn. I believe if I could get my current setup working nicely it would be easy to implement these other effects (as themable transitions). Committing to a control like Pivot won't get me any closer to that vision.
Here is my XAML:
<Grid x:Name="LayoutRoot" Background="Transparent" Height="{Binding RealHeight, ElementName=phoneApplicationPage}" Width="{Binding RealWidth, ElementName=phoneApplicationPage}" HorizontalAlignment="Left" VerticalAlignment="Top">
<ScrollViewer x:Name="SlideScroller" VerticalScrollBarVisibility="Disabled" Height="{Binding RealHeight, ElementName=phoneApplicationPage}" Margin="0,0,0,-31" ScrollViewer.HorizontalScrollBarVisibility="Auto" HorizontalAlignment="Left" VerticalAlignment="Top">
<StackPanel x:Name="SlidePanel" Orientation="Horizontal" Height="{Binding RealHeight, ElementName=phoneApplicationPage}" VerticalAlignment="Top" HorizontalAlignment="Left">
<Grid x:Name="Slide0" Margin="0" VerticalAlignment="Top" HorizontalAlignment="Left" Width="{Binding RealWidth, ElementName=phoneApplicationPage}" Height="{Binding RealHeight, ElementName=phoneApplicationPage}" Background="#FFCCCCCC">
<Image x:Name="Photo0" Width="{Binding RealWidth, ElementName=phoneApplicationPage}" Height="{Binding RealHeight, ElementName=phoneApplicationPage}" VerticalAlignment="Top" HorizontalAlignment="Left" Stretch="UniformToFill"/>
</Grid>
<Grid x:Name="Slide1" Margin="0" VerticalAlignment="Top" HorizontalAlignment="Left" Width="{Binding RealWidth, ElementName=phoneApplicationPage}" Height="{Binding RealHeight, ElementName=phoneApplicationPage}" Background="#FFCCCCCC">
<Image x:Name="Photo1" Width="{Binding RealWidth, ElementName=phoneApplicationPage}" Height="{Binding RealHeight, ElementName=phoneApplicationPage}" VerticalAlignment="Top" HorizontalAlignment="Left" Stretch="UniformToFill"/>
</Grid>
<Grid x:Name="Slide2" Margin="0" VerticalAlignment="Top" HorizontalAlignment="Left" Width="{Binding RealWidth, ElementName=phoneApplicationPage}" Height="{Binding RealHeight, ElementName=phoneApplicationPage}" Background="#FFCCCCCC">
<Image x:Name="Photo2" Width="{Binding RealWidth, ElementName=phoneApplicationPage}" Height="{Binding RealHeight, ElementName=phoneApplicationPage}" VerticalAlignment="Top" HorizontalAlignment="Left" Stretch="UniformToFill"/>
</Grid>
</StackPanel>
</ScrollViewer>
</Grid>
It turns out the setup I described works pretty well if I just prevent the ScrollViewer from getting manipulated directly by the user and position it manually. This eliminates the physics effects that were causing most of the glitchiness I mentioned.
XAML
<ScrollViewer x:Name="SlideScroller" VerticalScrollBarVisibility="Disabled" Height="{Binding RealHeight, ElementName=phoneApplicationPage}" ScrollViewer.HorizontalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Visible" HorizontalAlignment="Left" VerticalAlignment="Top">
<StackPanel x:Name="SlidePanel" Orientation="Horizontal" Height="{Binding RealHeight, ElementName=phoneApplicationPage}" VerticalAlignment="Top" HorizontalAlignment="Left">
</StackPanel>
</ScrollViewer>
<Rectangle x:Name="ScrollInterceptRect" Margin="0,0,0,-31" Width="{Binding RealWidth, ElementName=phoneApplicationPage}" Height="{Binding RealHeight, ElementName=phoneApplicationPage}" HorizontalAlignment="Left" VerticalAlignment="Top">
Codebehind
public MainPage()
{
InitializeComponent();
ScrollInterceptRect.MouseLeftButtonUp += new MouseButtonEventHandler(ScrollInterceptRect_MouseLeftButtonUp);
ScrollInterceptRect.MouseLeftButtonDown += new MouseButtonEventHandler(ScrollInterceptRect_MouseLeftButtonDown);
ScrollInterceptRect.MouseMove += new MouseEventHandler(ScrollInterceptRect_MouseMove);
}
//...
NavigationIndices navigationIndices = new NavigationIndices();
readonly double swipeThreshold = 80.0;
SwipeDirection swipeDirection;
bool tapCancelled;
Point swipeDelta;
Point swipeStartPosition;
double startScrollOffsetX;
void SlideScroller_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
swipeStartPosition = e.GetPosition(this);
startScrollOffsetX = SlideScroller.HorizontalOffset;
}
void ScrollInterceptRect_MouseMove(object sender, MouseEventArgs e)
{
Point touchPosition = e.GetPosition(this);
swipeDelta = new Point() { X = swipeStartPosition.X - touchPosition.X, Y = swipeStartPosition.Y - touchPosition.Y };
SlideScroller.ScrollToHorizontalOffset(startScrollOffsetX + swipeDelta.X);
// swipe right
if (swipeDelta.X > swipeThreshold)
{
swipeDirection = SwipeDirection.Left;
tapCancelled = true;
}
// swipe left
else if (swipeDelta.X < -swipeThreshold)
{
swipeDirection = SwipeDirection.Right;
tapCancelled = true;
}
}
void ScrollInterceptRect_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (swipeDirection == SwipeDirection.Left && navigationIndices.X < photos.Count - 1 && photos[navigationIndices.X] != null)
{
navigationIndices.X++;
}
// only go back when you aren't already at the beginning
else if (swipeDirection == SwipeDirection.Right && navigationIndices.X > 0)
{
navigationIndices.X--;
}
if (!tapCancelled)
{
// handle tap
}
else
{
animateScrollViewerToCurrentPhoto();
}
}
This is simplified a bit for clarity (I also use vertical swipe for something in my app and I omitted how I'm animating the ScrollViewer - probably worthy of it's own post).
I'd love to hear any improvements you can offer to this, or suggestions on better ways to implement it altogether. Perhaps extending the Panel Class or as a custom Behavior. Thoughts?

cloning the controls

I am designing a silverlight application in which i will have a rectangle control at the left side, when i click the rectangel and drag a copy of the rectangle control should be created and dragged and dropped in to the page.
Please can anyone help me with the code
For simplicity I'm going to leave out the Drag-Drop stuff since this question seems mainly about the cloning aspect.
The tool needed is the DataTemplate class. You place in a resource dictionary the set of items you want to clone each enclosed in a DataTemplate. You can use ContentPresenter to display instances of these items in say stack panel on the left. You can then use code to create instances of the template content and place them in say a Canvas on the right.
Example.
Xaml:-
<UserControl x:Class="SilverlightApplication1.CloningStuff"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
>
<UserControl.Resources>
<DataTemplate x:Key="Rectangle">
<Rectangle Stroke="Blue" StrokeThickness="3" Fill="CornflowerBlue" Width="100" Height="75" />
</DataTemplate>
</UserControl.Resources>
<Grid x:Name="LayoutRoot" Background="White">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel>
<ContentPresenter x:Name="Rectangle" ContentTemplate="{StaticResource Rectangle}" />
</StackPanel>
<Canvas x:Name="Surface" MouseLeftButtonDown="Surface_MouseLeftButtonDown" Grid.Column="1" Background="Wheat">
</Canvas>
</Grid>
</UserControl>
Code:-
public partial class CloningStuff : UserControl
{
public CloningStuff()
{
InitializeComponent();
}
private void Surface_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Canvas target = (Canvas)sender;
Point p = e.GetPosition(target);
Rectangle r = (Rectangle)((DataTemplate)Resources["Rectangle"]).LoadContent();
Canvas.SetLeft(r, p.X);
Canvas.SetTop(r, p.Y);
target.Children.Add(r);
}
}
This shows using a ContentPresenter to display your rectangle. In place of drag-dropping (for which there are plenty of examples of elsewhere) this code just creates a Clone of the rectangle whereever the user clicks in the Canvas.

Resources