Universal Windows 8.1 Store project here.
I want to know, when a ListView stops scrolling after user interaction. I found plenty of information on the net, but not one example reliably working on WP 8.1 (WPF/WP8 examples do not help much, and there are loads of them).
Here's what I do now.
1. The ListView
<ListView
x:Name="MessageList"
ItemsSource="{Binding Messages}"
VerticalAlignment="Bottom"
ItemContainerStyle="{StaticResource ChatListViewItemStyle}"
PointerEntered="MessageList_OnPointerEntered"
>
<ListView.ItemTemplate>
<DataTemplate>
<messages:MessageContainer />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
2. The ScrollViewer
I get a ScrollViewer reference from the ListView in code behind.
// GetChildElement<T>(this DependencyObject root) is a simple extension method of mine
Scroll = MessageList.GetChildElement<ScrollViewer>();
3. ListViewer.PointerEntered and ScrollViewer.ViewChanged
PointerEntered handler is used to detect the start of user interaction. When an interaction is detected, I subscribe to Scroll.ViewChanged and use IsIntermediate flag of the event to detect when the list stops scrolling (including inertia).
void MessageList_OnPointerEntered(object sender, PointerRoutedEventArgs e)
{
Debug.WriteLine("START MONITORING INTERACTION");
Scroll.ViewChanged += OnViewChangedByUser;
}
void OnViewChangedByUser(object sender, ScrollViewerViewChangedEventArgs e)
{
Debug.WriteLine("WAITING FOR INTERACTION TO END");
if (!e.IsIntermediate) {
Debug.WriteLine("INTERACTION ENDED");
Scroll.ViewChanged -= OnViewChangedByUser;
}
}
This does work to some extent.
The problem
The problem is, ViewChanged is not fired when the list is scrolled to the end/start and the user pulls it out of bounds and releases it, causing it to return back with inertia. So, the interaction start is detected, but the end is not. ViewChanged is not fired at all -- neither with IsIntermediate=True, nor with False.
What is a better way of doing what I want?
Sadly there's no good way to do this on Windows 8.1 aside from repeated polling and checking the ScrollOffset.
I'd just get an array of 10 doubles, and like 10 times a second I'd shift-in the current scroll offset. Than in that same handler check if the last 5 equals to the end of your list than raise an event.
As Tamás Deme puts it, there's no nice way of doing what is required. However, I've found a workaround that works in my case (nothing nice about it though).
In fact, I'm detecting, whether the list is scrolled to the bottom, when the scrolling stops. It's detecting the end of scrolling is what is causing so much trouble.
There are two parts of the problem: 1 - detecting the end of user interaction, 2 - detecting the end of inertia. Suprisingly, there's no good way of solving either of them. Thankfully, what I actually need is just knowing the value of VerticalOffset when scrolling (user-driven or inertia-animated) ceases. I don't actually have to know whether the user is still holding the list or not.
void MessageList_OnPointerEntered(object sender, PointerRoutedEventArgs e)
{
IsScrolledToLastLine = false; // this is to signal, that the user is
// holding the list, and there must be no
// automatic scrolling, when content is
// added to it.
Debug.WriteLine("[*]START MONITORING INTERACTION");
Scroll.ViewChanged += OnViewChangedByUser;
Scroll.LayoutUpdated += OnScrollLayoutUpdated;
}
void OnScrollLayoutUpdated(object sender, object e)
{
// will trigger multiple times during scrolling
// AND
// will trigger when inertia finally stops
// (regardless of the changes of VerticalOffset)
IsScrolledToLastLine = Scroll.ScrollableHeight == Scroll.VerticalOffset;
Debug.WriteLine("Interaction progress: {0}", IsScrolledToLastLine);
}
void OnViewChangedByUser(object sender, ScrollViewerViewChangedEventArgs e)
{
if (!e.IsIntermediate) {
IsScrolledToLastLine = Scroll.ScrollableHeight == Scroll.VerticalOffset;
Debug.WriteLine("Interaction end: {0}", IsScrolledToLastLine);
Scroll.LayoutUpdated -= OnScrollLayoutUpdated;
Scroll.ViewChanged -= OnViewChangedByUser;
}
}
Scroll.LayoutUpdated
LayoutUpdated is fired multiple times during scrolling. Unlike ViewChanged this event is also fired when inertia stops in the situation shown in the picture of the post. Unfortunatelly, there is no way to determine in LayoutUpdated, whether the list stopped scrolling completely or not.
ViewChanged works fine when you actually change VerticalOffset by scrolling; LayoutUpdated covers the over-scrolling situation.
There is another problem though: OnScrollLayoutUpdated may remain subscribed when scrolling over the edges of the list, as ViewChanged will not trigger. Fortunately, I can just ignore that, this doesn't break anything.
Related
I am making a UWP app which is supposed to be on xbox for now and maybe in future ill release it on pc and other platforms. I know that on PC and for mobile we can enable this feature with following 2 properties on the GridView or ListView.
CanReorderItems=True
CanDrop=True
But according to Microsoft Docs, drag and drop feature is not available or supported on xbox.
So what are any other options to achieve this reorder feature on xbox GridView?
UPDATE 1
So here is my backend code for the gridview. selection mode is single but I am not using selectionchanged event because that just creates lot of confusion and for now just assume that we always need to swap the items I will set the boolean later once the swapping in working perfectly.
private void SamplePickerGridView_ChoosingItemContainer(Windows.UI.Xaml.Controls.ListViewBase sender, ChoosingItemContainerEventArgs args)
{
if (args.ItemContainer != null)
{
return;
}
GridViewItem container = (GridViewItem)args.ItemContainer ?? new GridViewItem();
//should be xbox actually after pc testing
if (DeviceTypeHelper.GetDeviceFormFactorType() == DeviceFormFactorType.Desktop)
{
container.GotFocus += Container_GotFocus;
container.LostFocus += Container_LostFocus;
//container.KeyDown += Container_KeyDown;
}
args.ItemContainer = container;
}
private TVShow GotItem, LostItem;
private void Container_LostFocus(object sender, RoutedEventArgs e)
{
LostItem = OnNowAllGridView.ItemFromContainer(e.OriginalSource as GridViewItem) as TVShow;
GotItem = null;
}
private void Container_GotFocus(object sender, RoutedEventArgs e)
{
GotItem = OnNowAllGridView.ItemFromContainer(e.OriginalSource as GridViewItem) as TVShow;
if (GotItem != null && LostItem != null)
{
var focusedItem = GotItem;
var lostitem = LostItem;
var index1 = ViewModel.Source.IndexOf(focusedItem);
var index2 = ViewModel.Source.IndexOf(lostitem);
ViewModel.Source.Move(index1, index2);
}
LostItem = null;
}
u can try the code with adaptivegridview or just normal gridview of uwp if it works with that it should work with adaptivegridview as well.
Current Bheaviour items are swaped but the focus remains at same index.
Expected the focus should also move along with the item.
Your finding is true, drag and drop is not supported on Xbox out of the box (although when mouse support comes to Xbox in the future, I guess it will work).
So if you need this functionality, you will have to implement it manually from the start. One option would be to add a button, that will display on Xbox only and will read like Reorder Grid.
When this "reorder" mode were enabled, you have several solutions available.
The easiest solution for you would be to set the SelectionMode to Single and when a item is selected, you would bring it to fromt of the underlying collection.
collection.Remove( selectedItem );
collection.Insert( 0, selectedItem );
This bring to front solution was implemented on the Xbox One dashboard for reordering tiles.
Second option would be to set the SelectionMode to Multiple, where user would first select one item and then a second one. After that you could move the first selected item before the second selected:
collection.Remove( firstSelectedItem );
var targetIndex = collection.IndexOf( secondSelectedItem );
collection.Insert( targetIndex, firstSelectedItem );
The last solution is the most complex. With SelectionMode = Single you would select a single item and then observe the direction in which the user focus moves and move the tile "in real time". This is the most user friendly, but hardest to implement reliably.
Just as an outline of the third solution - you could capture the GotFocus event if you implement a custom template of the GridView:
<GridView.ItemsPanel>
<ItemsPanelTemplate>
<ItemsWrapGrid Orientation="Horizontal"
GotFocus="GridViewItem_GotFocus"/>
</ItemsPanelTemplate>
</GridView.ItemsPanel>
Now within this GotFocus handler you could retrieve the item that has currently focus from the EventArgs.OriginalSource. This way you could know which item got the focus and you could swap it with the item the user selected.
Update - hacky solution
I have come up with a hacky approach that solves the GotFocus/LostFocus mess.
The problem with GotFocus is that when we move the item in collection, the focus gets confused. But what if we didn't physically move the items at all?
Suppose your item type is TVShow. Let's create a wrapper around this type:
public class TVShowContainer : INotifyPropertyChanged
{
private TVShow _tvShow;
public TVShow TvShow
{
get => _tvShow;
set
{
_tvShow = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Now change the collection item type to this new "wrapper" type. Of course, you also have to update your GridView DataTemplate to have the right references. Instead of "{Binding Property}" you will now need to use "{Binding TvShow.Property}", or you can set the DataContext="{Binding TvShow}" attribute to the root element inside the DataTemplate.
But you may now see where I am going with this. Currently you are using Move method to move the items in the collection. Let's replace this with a swap:
var item1 = focusedItem.TvShow;
focusedItem.TvShow = LostItem.TvShow;
LostItem.TvShow = item1;
This is a big difference, because we no longer change the collection itself, but just move the references to items that are wrapped in a "static" container. And thanks to bindings the items will properly display where they should.
This is still a hacky solution, because it requires you to wrap your items just for the sake of the reordering, but it at least works. I am however still interested in finding a better way to do this.
From the docs:
Note When a user flips through FlipView content using touch
interaction, a SelectionChanged event occurs only when touch
manipulations are complete. This means that when a user flips through
content quickly, individual SelectionChanged events are not always
generated for every item because the manipulation is still occurring.
Is there a way to configure the FlipView control to fire SelectionChanged for each flip? This behavior makes implementing paging interesting as the user, if flipping fast enough, can flip to the end of the list before more items can be added.
One solution to the problem is to extend the FlipView and monitor its ScrollViewer. Here is a quick sample of what I'm suggesting. Seems to work on horizontal flip view (haven't handled any other cases, and haven't tested too much).
public class FixedFlipView : FlipView {
public ScrollViewer ScrollViewer {
get;
private set;
}
protected override void OnApplyTemplate() {
base.OnApplyTemplate();
this.ScrollViewer = (ScrollViewer)this.GetTemplateChild("ScrollingHost");
this.ScrollViewer.ViewChanged += ScrollViewer_ViewChanged;
}
void ScrollViewer_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e) {
var index = (int)this.ScrollViewer.HorizontalOffset - 2;
if (this.SelectedIndex != index) {
this.SelectedIndex = index;
}
}
}
Some things to note:
You may want to get the ScrollViewer in a different way that does not depend on its name. Like using the method in my answer here. Although, I'd guess this is fine, too.
It may be a better idea to use a separate event for this. In the code above I set the SelectedIndex property, which raises the SelectionChanged event, but it is also very likely to be doing other stuff as well, so it may be a problem in some cases.
I want to prevent the ability to deselect a list view item if it is already selected. Therefore how do I prevent right mouse click ability to deselect an item?
I have prevented the ability to deselect via swiping by using IsSwipeEnabled="False" on the List View. I didn't require swipe ability on the list view.
I'm happy to completely prevent right mouse click on the list view items if needed.
If I am reading your question correctly, it sounds like you want the ability for the user to select items, but to not be able to de-select. If that is the case, it seems like a strange requirement - it goes against normal UI convention and does something that the user is not expecting.
Having said that, you can do so by handling the SelectionChanged event in the ListView.
When the event is triggered, it gives you a list of removed (de-selected) items. You then just need to add those items back into the ListView's selected items list:
private void itemListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
foreach (var item in e.RemovedItems)
{
itemListView.SelectedItems.Add(item);
}
}
Note that if you use the above code, you do not have to handle any swipe or mouse events.
Edit - Per OP's comment, the requirement is slightly different than what I thought:
I want the selected item to deselect if a different item is selected. however what I dont want is an already selected item to be (manually) deselected
Assuming that you have a single select ListView, you can still use the SelectionChanged event and the SelectionChangedEventArgs to do what you are asking for:
private void itemListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.RemovedItems.Count > 0 && e.AddedItems.Count == 0)
{
var removed = e.RemovedItems[0];
itemListView.SelectedItem = removed;
}
}
I have found a simple solution on the following forum.
You simply add a RightTapped event to the ListView DataTemplate content.
<ListView>
<ListView.ItemTemplate>
<DataTemplate>
<ContentPresenter RightTapped="daves_RightTapped" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
And then in the code behind:
private void daves_RightTapped(object sender, Windows.UI.Xaml.Input.RightTappedRoutedEventArgs e)
{
e.Handled = true;
}
This works fine on an outlook style ListView.
I registered to the OnSizeChanged event of my Page, like that:
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
ApplicationViewState myViewState = ApplicationView.Value;
if (myViewState == ApplicationViewState.Snapped)
{
Windows.UI.ViewManagement.ApplicationView.TryUnsnap();
}
}
I'm tyring to set the application-view to Filled/Portrait state when the user trying (manually....) to resize it to snapped view.
but the TryUnsnap method fails and it stays in snapped state...
Help!
Thanks.
To understand TryUnsnap() we need to understand the 2 types of Windows 8 events:
Programmatic events
Programmatic events do not require the user to do anything. For example the Loaded event of a Page or the Tick event of a Timer.
User-initiated events
User-initiated events require the user to do something. For example the Click event of a Button or the Tapped event of a Control.
The important part
Depending on the type of event, only certain Windows 8 APIs can be called. Adding a Secondary Tile, for example. And (as you might have guessed) un-Snapping an app.
That means you can call those APIs all you want from programmatic events but they will never deliver the results you desire. Unsnap in the StateChanged event, and it will fail for this reason. Unsnap in the Button.Click event, and it will succeed for this reason.
The rationale behind this behavior is the user experience. If the app can change it's 'orientation' on the user without the user's interaction then the behavior of the app becomes both confusing and unpredictable. Windows 8 is a pro-user operating system. When you discover developer 'constraints', 99% of the time it is this philosophy behind it.
Let me demonstrate:
If you attached to the StateChanged event, your code would look like this:
this.ApplicationViewStates.CurrentStateChanged += (s, args) =>
{
System.Diagnostics.Debug.WriteLine("After StateChanged: {0}", this.ApplicationViewStates.CurrentState.Name);
if (this.ApplicationViewStates.CurrentState == this.Snapped)
{
System.Diagnostics.Debug.WriteLine("Before Unsnap: {0}", this.ApplicationViewStates.CurrentState.Name);
Unsnap();
}
};
However, the resulting output (in the debugger) would look like this:
After StateChanged: FullScreenLandscape
After StateChanged: Snapped
Before Unsnap: Snapped
After TryUnsnap: Snapped
This is frustrating for a developer who does not understand the difference between programmatic and user-initiated events in Windows 8. The API appears to "not work" when, in fact, it works perfectly. Just not like they want it to.
If you attached to the Click event, your code would look like this:
MyButton.Click += (s, args) =>
{
System.Diagnostics.Debug.WriteLine("After Button.Click: {0}", this.ApplicationViewStates.CurrentState.Name);
if (this.ApplicationViewStates.CurrentState == this.Snapped)
{
System.Diagnostics.Debug.WriteLine("Before Unsnap: {0}", this.ApplicationViewStates.CurrentState.Name);
Unsnap();
}
};
Then, the resulting output would look like this:
After Button.Click: Snapped
Before Unsnap: Snapped
After TryUnsnap: Snapped
After StateChanged: FullScreenLandscape
This gets you what you want, but it brings up an important point. See how After TryUnsnap the state REMAINS "Snapped"? The transition of a Visual State from one to another is not a synchronous event. Calling for a change takes an unpredictable amount of time. It's probably done with a dispatch post, but I would have to check to be sure.
Having said all that, the state does change. And, after the change the CurrentStateChanged event is raised and you can handle the new Snapped state. By the way, it does not matter if there is another snapped app, this works either way.
The MSDN docs say it only works when it is in the foreground. This is pretty stupid since user interaction can't occur on a background app, and background apps have their threads suspended anyway. But, to be fair to MSDN, this API does not work when your app is in the background - whatever that's worth.
I hope this helps clear it up.
And now to your question:
You want to go from Snapped to Portrait? Of course in Portrait, Snapped is not possible so this is not a possibility for you to code. You want to go from Snapped to Filled as soon as the app is snapped. The event raised from the Snapped action is a programmatic event. As a result, you have to lure the user into doing something in your UI first. So, no you can't do what you are asking. You can't Unsnap() until the user interacts with your app somehow (like a button click event).
Oh, and here's the Unsnap() method if you wanted to reference all my code. I am not doing anything special, but you might be interested:
void Unsnap()
{
if (Windows.UI.ViewManagement.ApplicationView.TryUnsnap())
// successfully unsnapped
System.Diagnostics.Debug.WriteLine("After TryUnsnap: {0}", this.ApplicationViewStates.CurrentState.Name);
else
// un-successfully unsnapped
System.Diagnostics.Debug.WriteLine("After TryUnsnap: {0}", this.ApplicationViewStates.CurrentState.Name);
}
Have a great day and best of luck!
var CurrentSnappedState = ApplicationView.Value;
if (CurrentSnappedState == ApplicationViewState.Snapped && !ApplicationView.TryUnsnap())
{
return;
}
Should do the trick. Remember that you can still snap the page, but when you try to do anything in the snapped page you will be redirected to the fullview.
First, a screenshot:
The title and image explain it pretty well. I have an ad set on the right side of my app's main group view (very very similar to the default grid template in this example), and when I pull up my About screen, the ad bleeds through.
The About screen is a user control set on a SettingsFlyout that I borrowed from some code samples handed out at a dev-camp (below).
class SettingsFlyout
{
private const int _width = 346;
private Popup _popup;
public void ShowFlyout(UserControl control)
{
_popup = new Popup();
_popup.Closed += OnPopupClosed;
Window.Current.Activated += OnWindowActivated;
_popup.IsLightDismissEnabled = true;
_popup.Width = _width;
_popup.Height = Window.Current.Bounds.Height;
control.Width = _width;
control.Height = Window.Current.Bounds.Height;
_popup.Child = control;
_popup.SetValue(Canvas.LeftProperty, Window.Current.Bounds.Width - _width);
_popup.SetValue(Canvas.TopProperty, 0);
_popup.IsOpen = true;
}
private void OnWindowActivated(object sender, Windows.UI.Core.WindowActivatedEventArgs e)
{
if (e.WindowActivationState == Windows.UI.Core.CoreWindowActivationState.Deactivated)
{
_popup.IsOpen = false;
}
}
void OnPopupClosed(object sender, object e)
{
Window.Current.Activated -= OnWindowActivated;
}
}
And, because I know it will be asked for, here is the line of XAML defining the ad on my page:
<ads:AdControl Visibility="{Binding IsTrial, Source={StaticResource License}, Converter={StaticResource BooleanToVisibilityConverter}}" Grid.Row="0" Grid.RowSpan="2" x:Name="LandscapeAdControl" ApplicationId="test_client" AdUnitId="Image_160x600" Width="160" Height="600" VerticalAlignment="Center" HorizontalAlignment="Right"/>
So, why is this happening, and how do I prevent it?
Suspicions
I am still on Consumer Preview b/c I have a show-and-tell Monday and didn't have time to work on migrating the OS on this box without risking being non-functional when I am showing this. As such, upgrading might fix it if it's a bug.
1.a. Update I have upgraded to Release Preview and have the same issue.
Is there some fancy ad-hiding-but-still-getting-impressions prevention technique at play here? Perhaps it thinks I am trying to cover the ad with a ui element and still get credit for it's impression without the user seeing it. If so, how do I manage this entirely legit use case?
Spoiler Alert: ZIndex isn't set anywhere.
It presents the same problem with overlaying the AppBar (top or bottom). I used the Opened and Closed events on the AppBar instance to hide/show the ad. This means the AdControl is bound to a local page property instead of binding directly to a ViewModel. Like you said, it's unfortunate but it works.
private void bottomAppBar_Opened(object sender, object e)
{
if (App.ViewModel.IsTrialVisibility == Visibility.Visible)
this.DefaultViewModel["AdVisibility"] = Visibility.Collapsed;
// else do nothing as we don't want to show it since it's not a trial
}
private void bottomAppBar_Closed(object sender, object e)
{
if(App.ViewModel.IsTrialVisibility == Visibility.Visible)
this.DefaultViewModel["AdVisibility"] = Visibility.Visible;
// else do nothing as it's not shown in the first place (not a trial)
}
There is a property on AdControl named: UseStaticAnchor
Setting this property to true will fix both performance problems with scrolling, as well as the AdControl drawing on top of everything else.
Original answer - this method is now outdated:
The AdControl has two methods on it: Suspend() and Resume().
Whenever you open a popup window or AppBar, you will want to call Suspend(), and Resume() when it is closed again.
I believe under the covers, the AdControl uses a WebView to display the ads. For whatever reason, a WebView will always display on top of everything else in your application. The fix for this is to temporarily disable the WebView, and instead display a WebViewBrush.
(This technique is described here: http://msdn.microsoft.com/library/windows/apps/windows.ui.xaml.controls.webviewbrush) So when you call Suspend() and Resume(), the AdControl is doing this under the covers.
What I've ended up doing is creating a UserControl (named SuspendingAdControl) that simply contains an AdControl and can be used anywhere in the app. Then whenever a window is opened or closed, I use Caliburn Micro's EventAggregator to publish an event. The SuspendingAdControl will subscribe and handle these events, and then appropriately call AdControl.Suspend() or Resume().
I ended up crafting some code to listen to an event on the flyout when it closed so I could high/show the ads manually. It's unfortunate that I had to do a workaround, but it works.
None of this is now necessary, as the flyout in 8.1 now is at the top of the Z-order.
I am still on Consumer Preview b/c I have a show-and-tell Monday and
didn't have time to work on migrating the OS on this box without
risking being non-functional when I am showing this. As such,
upgrading might fix it if it's a bug.
I haven't used any advertisements in my own metro applications yet, so I haven't seen any problems like this occurring. I'm using the Release Preview, and was using Consumer Preview prior to May 2nd.
There were some significant changes between the Consumer Preview and Release Preview. As such, upgrading might fix this, or it may break something else.
You're going to have to upgrade eventually. I'd suggest trying that first before you attempt to solve the problem.