In my app I have one main ScrollViewer and many DataGrids with embedded scrollviewers in them. Thanks to this article https://serialseb.com/blog/2007/09/03/wpf-tips-6-preventing-scrollviewer-from/ and comments below I made parent and children scrollviwers work fine for me. Every time I start scrolling with mouse my parent scroll starts work when it reaches to any DataGrid then DataGrid's scrollviewer does it work and when whole content is scrolled it returns scrolling to the parent (main ScrollViewer). But that logic is not enough for me. I need this: if I start scrolling DataGrid (using child scrollviewer) it will scroll ONLY this DataGrid (when child ScrollViwer can't scroll any more it doesn't return scrolling to parent), but only if there is something to scroll on it. If not, the main window will scroll again. And if I start scrolling the main content than any of child ScrolViewers shouldn't react.
My questions is: how can I detect what UIElement start scrolling it's content and continue to scroll only it's content?
Simply put I want the same scroll parent/child logic as we have here on stackoverflow (try to scroll elements with code and whole page).
I think the similar idea is in this question WPF detect scrolling parent Control but it's not suitable for me because I have DataGrid instead of Popup.
I think about IsFocusable property but honestly I don't know where in my code I can check it.
Here is my code:
public class ScrollViewerCorrector
{
public static void SetApplyScrolling(DependencyObject obj, bool value)
{
obj.SetValue(ApplyScrollingProperty, value);
}
public static readonly DependencyProperty ApplyScrollingProperty =
DependencyProperty.RegisterAttached("ApplyScrolling", typeof(bool), typeof(ScrollViewerCorrector),
new FrameworkPropertyMetadata(false, OnApplyScrollingPropertyChanged));
public static void OnApplyScrollingPropertyChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var viewer = sender as ScrollViewer;
if (viewer == null) return;
if ((bool)e.NewValue) viewer.PreviewMouseWheel += OnPreviewMouseWheel;
else viewer.PreviewMouseWheel -= OnPreviewMouseWheel;
}
private static List<MouseWheelEventArgs> _wheelEventArgsList = new List<MouseWheelEventArgs>();
private static void OnPreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
var scrollControl = sender as ScrollViewer;
if (!e.Handled && sender != null && !_wheelEventArgsList.Select(args => (args.Source)).Contains(sender))
{
var previewEventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta)
{
RoutedEvent = UIElement.PreviewMouseWheelEvent, Source = sender
};
var originalSource = e.OriginalSource as UIElement;
_wheelEventArgsList.Add(previewEventArg);
originalSource.RaiseEvent(previewEventArg);
_wheelEventArgsList.Remove(previewEventArg);
//check if any of children handled the event
if (!previewEventArg.Handled && ((e.Delta > 0 && scrollControl.VerticalOffset == 0) ||
(e.Delta <= 0 && scrollControl.VerticalOffset >=
scrollControl.ExtentHeight - scrollControl.ViewportHeight)))
{
e.Handled = true;
var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);
eventArg.RoutedEvent = UIElement.MouseWheelEvent;
eventArg.Source = sender;
var parent = (UIElement)((FrameworkElement)sender).Parent;
parent.RaiseEvent(eventArg);
}
}
}
MainView.xaml
<rui:ReactiveUserControl x:TypeArguments="viewModels:MainViewModel"
x:Class="AnalyticalReporting.UI.Views.MainView"
xmlns:viewModels="clr-namespace:AnalyticalReporting.UI.ViewModels"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:rui="http://reactiveui.net"
xmlns:controls="clr-namespace:MNS.Common.UI.Controls;assembly=MNS.Common.UI"
mc:Ignorable="d"
d:DesignHeight="1024" d:DesignWidth="1280"
Background="{DynamicResource WhiteBrush}">
<UserControl.Resources>
<ResourceDictionary>
<Style TargetType="{x:Type ScrollViewer}">
<Style.Setters>
<Setter Property="controls:ScrollViewerCorrector.ApplyScrolling" Value="True" />
</Style.Setters>
</Style>
<DataTemplate x:Key="ListTemplate">
<StackPanel>
<rui:ViewModelViewHost x:Name="DisplayingViewModelViewHost" HorizontalContentAlignment="Stretch" ViewModel="{Binding .}"/>
</StackPanel>
</DataTemplate>
</ResourceDictionary>
</UserControl.Resources>
<ScrollViewer x:Name="MainScrollViewer" VerticalScrollBarVisibility="Auto" VerticalAlignment="Top">
<ItemsControl x:Name="InfoCardList" ItemTemplate="{StaticResource ListTemplate}" >
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</ScrollViewer>
</rui:ReactiveUserControl>
Related
I have situation where I want an overlay control to block UI interactions on a Page for everything that is behind a border. I have tried setting Border.ManipulationMode to False. I have set IsTapEnabled, IsRightTapEnabled, IsDoubleTapEnabled, and IsHitTestVisible to False.
I also tried subscribing to the Tapped and PointerEntered events, and setting the args Handled property to true. After all of this I can still click on Buttons through the border, and invoke their commands. Below are a few screenshots for context:
Page with no overlay
Page now has what should be an overlay that blocks controls behind it
A button capturing PointerOver that shouldn't be
Here is the UserControl xaml that becomes the overaly on the Page:
<UserControl x:Class="PocMvvmToolkitApp.Dialogs.DialogShell"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="400">
<Grid x:Name="overlayGrid"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<!--dialogShield is the Border that I want to prevent click through on-->
<Border x:Name="dialogShield"
Background="#AAFFFFFF"
IsHitTestVisible="False"
ManipulationMode="None"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
IsDoubleTapEnabled="False"
IsHoldingEnabled="False"
IsRightTapEnabled="False"
IsTapEnabled="False"/>
<Border x:Name="dialogBorder"
BorderBrush="Black"
BorderThickness="1" />
</Grid>
Attempting to handle the events:
public DialogShell()
{
this.InitializeComponent();
this.allDialogs = new List<ExtendedContentDialog>();
this.visibleDialogs = new List<ExtendedContentDialog>();
////Doesn't work
this.dialogShield.PointerEntered += this.OnModalShieldPointerEntered;
this.dialogShield.Tapped += this.OnModalShieldTapped;
}
private void OnModalShieldTapped(object sender, Windows.UI.Xaml.Input.TappedRoutedEventArgs e)
{
////Doesn't block click through
e.Handled = true;
}
private void OnModalShieldPointerEntered(object sender, Windows.UI.Xaml.Input.PointerRoutedEventArgs e)
{
e.Handled = true;
}
On the Page.xaml.cs here is where I add or remove the DialogShell control to the parent Grid on the page:
private void OnDialogStackChanged(Args.DialogStackChangedEventArgs args)
{
switch (args.Context)
{
case Args.DialogStackChangedContext.Showing:
if (this.dialogShell == null)
{
this.dialogShell = new DialogShell();
this.dialogShell.ShowDialog(args.Dialog);
this.rootGrid.Children.Add(this.dialogShell);
Grid.SetColumnSpan(this.dialogShell, 2);
}
break;
case Args.DialogStackChangedContext.Closing:
if (this.dialogShell != null)
{
this.dialogShell.RemoveDialog(args.Dialog);
if (this.dialogShell.AllDialogs.Count == 0)
{
this.rootGrid.Children.Remove(this.dialogShell);
this.dialogShell = null;
}
}
break;
}
}
Any help with this Border situation would be appreciated. Before someone recommends using ContentDialog, please don't, I have my reasons for this setup. Thanks!
I have a piece of code with two Border elements, but the hit-testing only works for the topmost Border (Border2) in the code below. This means that when I right-click, I see the message box, but when I left-click, nothing happens. Is there a way to fix this so that I can capture different mouse events with sibling controls that have different Z-index values? Here is my code:
<Window x:Class="HiTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:HiTest"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<local:Border1 Background="Transparent"/>
<local:Border2 Background="Transparent"/>
</Grid>
</Window>
public class Border1 : Border
{
public Border1()
{
MouseLeftButtonDown += Border1_MouseLeftButtonDown;
}
private void Border1_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
MessageBox.Show("Left");
}
}
public class Border2 : Border
{
public Border2()
{
MouseRightButtonDown += Border2_MouseRightButtonDown;
}
private void Border2_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
MessageBox.Show("Right");
}
}
In summary, I want to be able to capture different mouse events with sibling controls that have different Z-index values. How can I achieve this?
it is not possible to capture events for sibling elements. Bubbling only works for elements with parent and child relation.
Something like this with parent child relation.
<Grid Name="Parent">
<Border Name="Border01" Background="Transparent" MouseDown="Border01_MouseDown">
<Border Name="Border02" Background="Transparent" MouseDown="Border01_MouseDown" />
</Border>
</Grid>
private void Border01_MouseDown(object sender, MouseButtonEventArgs e)
{
if (sender.GetType() == typeof(Grid)) { }
if (sender.GetType() == typeof(Border))
{
if (((Border)sender).Name == "Border01" & e.LeftButton == MouseButtonState.Pressed)
{
MessageBox.Show("Left & Button01");
}
if (((Border)sender).Name == "Border02" & e.RightButton == MouseButtonState.Pressed)
{
MessageBox.Show("Right & Button02");
}
}
}
If you have sibling elements you need to delegate the MouseDown from the top element to the lower element by yourself.
I have the following TabControl:
<TabControl ItemsSource="{Binding Tabs"}>
<TabControl.ContentTemplate>
<DataTemplate DataType="{x:Type vm:TabVM}">
<TextBox></TextBox>
<TextBox Text="{Binding SomeProperty}"></TextBox>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
The unexpected behaviour is that first TextBox has Text property shared between all tabitems, while second TextBox effectively bind to ViewModel property.
My need is to make independent the first TextBox too, even without binding.
What can I do ?
** UPDATE **
After several tries I've decided to use the ikriv's TabContent.cs.
The only issue I've found with this is that calling the TabControl.Items.Refresh() (i.e. after removing a tabItem) cause the reset of the internal cache.
An unelegant but effective solution may be this:
public ContentManager(TabControl tabControl, Decorator border)
{
_tabControl = tabControl;
_border = border;
_tabControl.SelectionChanged += (sender, args) => { UpdateSelectedTab(); };
/* CUSTOM */
var view = CollectionViewSource.GetDefaultView(((TabControl)_tabControl).Items);
view.CollectionChanged += View_CollectionChanged;
}
/*
* This fix the internal cache content when calling items->Refresh() method
* */
private void View_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.OldItems != null)
{
/* Retrieve all tabitems cache and store to a temp list */
IList<ContentControl> cachedContents = new List<ContentControl>();
foreach (var item in _tabControl.Items)
{
var tabItem = _tabControl.ItemContainerGenerator.ContainerFromItem(item);
var cachedContent = TabContent.GetInternalCachedContent(tabItem);
cachedContents.Add(cachedContent);
}
/* rebuild the view */
_tabControl.Items.Refresh();
/* Retrieve all cached content and store to the tabitems */
int idx = 0;
foreach (var item in _tabControl.Items)
{
var tabItem = _tabControl.ItemContainerGenerator.ContainerFromItem(item);
TabContent.SetInternalCachedContent(tabItem, cachedContents[idx++]);
}
}
}
You should use data binding since the same ContentTemplate will be applied for all items in your ItemsSource. Only the binding will be refreshed when you switch tabs basically. The TextBox isn't re-created nor reset.
What can I do ?
You could work around this in the view by handling the SelectionChanged event of the TabControl and reset the TextBox control yourself:
private void tabs_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
TabControl tc = sender as TabControl;
ContentPresenter cp = tc.Template.FindName("PART_SelectedContentHost", tc) as ContentPresenter;
if(cp != null && VisualTreeHelper.GetChildrenCount(cp) > 0)
{
ContentPresenter cpp = VisualTreeHelper.GetChild(cp, 0) as ContentPresenter;
if(cpp != null)
{
TextBox textBox = cpp.FindName("txt") as TextBox;
if (textBox != null)
textBox.Text = string.Empty;
}
}
}
<TabControl x:Name="tabs" ItemsSource="{Binding Tabs}" SelectionChanged="tabs_SelectionChanged">
<TabControl.ContentTemplate>
<DataTemplate>
<ContentPresenter>
<ContentPresenter.Content>
<StackPanel>
<TextBox x:Name="txt"></TextBox>
</StackPanel>
</ContentPresenter.Content>
</ContentPresenter>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
If you want to persist the text in the TextBox when you switch tabs you could use the attached behaviour from the following article and set its IsCached property to true: https://www.codeproject.com/articles/460989/wpf-tabcontrol-turning-off-tab-virtualization
<TabControl ItemsSource="{Binding Items}" behaviors:TabContent.IsCached="True">
<!-- Make sure that you don't set the TabControl's ContentTemplate property but the custom one here-->
<behaviors:TabContent.Template>
<DataTemplate>
<StackPanel>
<TextBox />
</StackPanel>
</DataTemplate>
</behaviors:TabContent.Template>
</TabControl>
Yet another approach would be to modify the ControlTemplate of the TabControl to include a ListBox as suggested by 'gekka' in the following thread on the MSDN forums: https://social.msdn.microsoft.com/Forums/en-US/4b71a43a-26f5-4fef-8dc5-55409262298e/using-uielements-on-datatemplate?forum=wpf
With the following XAML:
<Window x:Class="MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="600" Width="640">
<ScrollViewer PanningMode="Both">
<StackPanel>
<TextBlock TextWrapping="Wrap">LOTS OF TEXT...</TextBlock>
<DataGrid MinHeight="200">
<DataGrid.Columns>
<DataGridTextColumn Width="100"></DataGridTextColumn>
<DataGridTextColumn Width="100"></DataGridTextColumn>
<DataGridTextColumn Width="100"></DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
<TextBlock TextWrapping="Wrap">LOTS OF TEXT...</TextBlock>
</StackPanel>
</ScrollViewer>
</Window>
You can scroll by touching on the TextBlocks. However, if you touch the DataGrid and attempt to scroll, it does nothing.
I'm guessing it has something to do with the fact that the content in the DataGrid is potentially scrollable itself so WPF is getting confused with the potentially nested scrollbars.
The desired behaviour is that touching in the DataGrid will scroll the content inside the DataGrid first (if necessary). Then, when content in the DataGrid has been fully scrolled, the main window will scroll.
Similar to PreviewMouseWheel (Bubbling scroll events from a ListView to its parent), you can do same with Touch controls:
C#:
public sealed class SubControlsTouchScrollEvent : Behavior<UIElement>
{
double originalDistance;
double actualDistance;
int delta;
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.PreviewTouchDown += AssociatedObject_PreviewTouchDown;
AssociatedObject.PreviewTouchMove += AssociatedObject_PreviewTouchMove;
}
protected override void OnDetaching()
{
AssociatedObject.PreviewTouchUp -= AssociatedObject_PreviewTouchDown;
AssociatedObject.PreviewTouchMove -= AssociatedObject_PreviewTouchMove;
base.OnDetaching();
}
void AssociatedObject_PreviewTouchDown(object sender, TouchEventArgs e)
{
System.Windows.IInputElement s = sender as System.Windows.IInputElement;
originalDistance = e.GetTouchPoint(s).Position.Y;
}
void AssociatedObject_PreviewTouchMove(object sender, TouchEventArgs e)
{
ScrollViewer s = sender as ScrollViewer;
actualDistance = e.GetTouchPoint(s).Position.Y;
delta = Convert.ToInt16(actualDistance - originalDistance);
s.ScrollToVerticalOffset(s.VerticalOffset - (delta * 0.1));
e.Handled = true;
}
}
XAML:
<ScrollViewer PanningMode="Both" Background="Transparent">
<interactivity:Interaction.Behaviors>
<local:SubControlsTouchScrollEvent />
</interactivity:Interaction.Behaviors>
</ScrollViewer>
NOTE: This is only for vertical scrolling, but you can add horizontal scrolling in the same way.
The solution from #neo_st worked for me. Three important sidenotes to make this work:
The Behavior needs to be attached to the outer ScrollViewer.
This solution can't handle MultiTouch. If you put two fingers on the touch screen the Events get fired by both fingers and the vertical scroll doesn't work correctly.
To get the typical vertical scroll behaviour on the outer ScrollViewer you should change the actualDistance to the original distance as follows:
void AssociatedObject_PreviewTouchMove(object sender, TouchEventArgs e)
{
ScrollViewer s = sender as ScrollViewer;
actualDistance = e.GetTouchPoint(s).Position.Y;
delta = Convert.ToInt16(actualDistance - originalDistance);
s.ScrollToVerticalOffset(s.VerticalOffset - delta);
originalDistance = actualDistance;
e.Handled = true;
}
I followed this link Detect when WPF listview scrollbar is at the bottom?
But ScrollBar.Scroll doesn't exist for ListView in Windows 10 .. how to achieve this requirement in windows 10
Thanks
Use the methods described here How can I detect reaching the end of a ScrollViewer item (Windows 8)?
If you want incremental loading there's a built-in solution for that. Otherwise go and traverse the visual tree for the scrollviewer object in the template.
You can do it by simply detecting VerticalOffset and ScrollableHeight of your ScrollViewer. Here is my simple code.
XAML:
<!--Apply ScrollViwer over the ListView to detect scrolling-->
<ScrollViewer Name="ContentCommentScroll" Grid.Row="2"
ViewChanged="ContentCommentScroll_ViewChanged" ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.HorizontalScrollMode="Disabled" ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<!--My Dynamic ListView-->
<ListView Name="MyDataList" ItemsSource="{Binding MyList}"
ItemTemplate="{StaticResource MyDataTemplate}"
ItemContainerStyle="{StaticResource myStyle}"/>
</ScrollViewer>
And code in its XAML.CS:
// Hold ScrollViwer
public ScrollViewer listScrollviewer = new ScrollViewer();
// Binded event, which will trigger on scrolling of ScrollViewer
private void ContentCommentScroll_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
{
Scrolling(ContentCommentScroll);
}
private void Scrolling(DependencyObject depObj)
{
ScrollViewer myScroll= GetScrollViewer(depObj);
// Detecting if ScrollViewer is fully vertically scrolled or not
if (myScroll.VerticalOffset == myScroll.ScrollableHeight)
{
// ListView reached at of its end, when you are scrolling it vertically.
// Do your work here here
}
}
public static ScrollViewer GetScrollViewer(DependencyObject depObj)
{
if (depObj is ScrollViewer) return depObj as ScrollViewer;
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
var child = VisualTreeHelper.GetChild(depObj, i);
var result = GetScrollViewer(child);
if (result != null) return result;
}
return null;
}
You can also apply similar logic to detect that if ListView Horizontally reached to its end.
You can wrap it with a ScrollViewer and name it. Then you can use some other methods.