I observed a strange behavior in my UWP Windows 10 app. Even if I navigate back from a page and the page is unloaded, the page still continues to emit events. For example, the old page which I navigated from, still emits "LayoutUpdated" event even if I go to a completely different page.
I prepared a minimal example to demonstrate this (code is below). It is pretty simple:
There are 2 pages: MainPage and ExamplePage. You can go from MainPage to ExamplePage and you can go back from ExamplePage to MainPage.
Every time you navigate to ExamplePage, a new ID is given to that newly created page (page is not cached).
A Grid in ExamplePage emits LayoutChanged event. And event handler writes a text to Debug console like: "grid layout updated on page 0". 0 is the page ID I gave to that page.
If you go back and forth a few times, you will see that the old pages still write layout updated text to the console. For example, if I go to page with ID 3, it writes to the console:
grid layout updated on page 0
grid layout updated on page 3
grid layout updated on page 1
grid layout updated on page 2
notice that the old pages still update their layouts. The old pages, should not emit any events anymore but they keep emitting events although there is no way to navigate to them anymore and they are unloaded.
Here is the code, there are 5 files, just create a new UWP project in VS2015 and then:
MainPage.xaml
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Button x:Name="NavigationButton"
Click="NavigationButton_Click"
HorizontalAlignment="Center"
VerticalAlignment="Top"
Margin="0,20,0,0">Navigate</Button>
</Grid>
MainPage.xaml.cs
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
namespace App7
{
public sealed partial class MainPage : Page
{
private App app;
public MainPage()
{
this.InitializeComponent();
app = (App)Application.Current;
}
private void NavigationButton_Click(object sender, RoutedEventArgs e)
{
var viewModel = new ExamplePageViewModel(app.GetPageId());
Frame.Navigate(typeof(ExamplePage), viewModel);
}
}
}
ExamplePage.xaml
<Grid x:Name="MainGrid"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
LayoutUpdated="MainGrid_LayoutUpdated">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Button x:Name="NavigationButton"
Click="NavigationButton_Click" HorizontalAlignment="Center"
Margin="0,20,0,0">Go Back</Button>
<TextBlock Text="{Binding PageId}"
Grid.Row="1"
FontSize="30"
HorizontalAlignment="Center"></TextBlock>
</Grid>
ExamplePage.xaml.cs
using System.Diagnostics;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;
namespace App7
{
public sealed partial class ExamplePage : Page
{
private ExamplePageViewModel viewModel;
public ExamplePage()
{
this.InitializeComponent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
if (e.NavigationMode == NavigationMode.New ||
e.NavigationMode == NavigationMode.Back)
{
viewModel = (ExamplePageViewModel)e.Parameter;
DataContext = viewModel;
}
}
private void NavigationButton_Click(object sender, RoutedEventArgs e)
{
Frame.GoBack();
}
private void MainGrid_LayoutUpdated(object sender, object e)
{
Debug.WriteLine("grid layout updated on page " + viewModel?.PageId.ToString());
}
}
}
ExamplePageViewModel.cs
using System.ComponentModel;
using Windows.UI.Xaml;
namespace App7
{
public class ExamplePageViewModel : INotifyPropertyChanged
{
private App app;
private int pageId;
public event PropertyChangedEventHandler PropertyChanged;
public int PageId
{
get
{
return pageId;
}
}
public ExamplePageViewModel(int pageId)
{
app = (App)Application.Current;
this.pageId = pageId;
}
}
}
Note: The viewmodel is just to see clearly which page is still emiting events. You can remove the viewmodel, and it won't change the problem.
The LayoutUpdated event will be fired for elements that are not in the main visual tree, provided that the element has not been collected by the garbage collector. Since we don't know the implementation of the Frame class, we don't know how it references the pages it instantiates (maybe it holds a reference to unloaded pages for slightly longer than it needs to? Who knows).
This, together with the asynchronous nature of the garbage collector, means that old pages can still raise LayoutUpdated events until either the event handler is removed or the object is collected by the GC. In your example, the GC simply hasn't gotten around to collecting your old pages.
Doesn't this make app performance drop if you have several complex pages still on memory? I can see on my app, that tens of complex pages are still firing the LayoutUpdated event, so all the controls are calculating their sizes on every page navigation, right?
Those pages should be collected by the GC during the next garbage collection cycle, which will automatically occur at some point when it is necessary to do so. You can force a garbage collection with GC.Collect(), but I don't recommend this. The GC is better at determining times to perform a collection than you are (generally).
The LayoutUpdated event is fired on all elements (I think), regardless of whether or not that particular element's layout has changed. If you read the docs for that event, it explains that it is necessary to do this in case the element's layout is affected by a sibling (for example).
The layout system is quite optimized. I don't think complex layout passes are performed for all elements every time they receive a LayoutUpdated event, so I wouldn't worry about that. However, it is important to make sure you're not doing unnecessary calculations in these event handlers when the element isn't visible.
Here is another page which explains the LayoutUpdated event quite well. It is a static event, which gets fired if any element anywhere had its layout updated.
I won't put any unnecessary code to LayoutUpdated event or I will unbind them on navigating back but still it will recalculate all the control's sizes on its own, right?
Your response to the LayoutUpdated event should be "some element somewhere had its layout updated which may or may not affect me". You could unbind the event, or you can just check if this.Parent or this.Frame is null and bail out, in which case the Page isn't in the Frame.
Are there any other events that fire even if the control is not in the visual tree? How can I find a list for such events?
I'm not sure. You need to test your app for these situations, just put a breakpoint in each event handler so you know if it's getting fired.
Related
.net maui app.
Dragging value element along the slider bar does not work if the the slider put into CarouselView's template like this:
<CarouselView ItemsSource="{Binding Items}">
<CarouselView.ItemTemplate>
<DataTemplate>
<Slider Minimum="0" Maximum="30" WidthRequest="200" />
</DataTemplate>
</CarouselView.ItemTemplate>
</CarouselView>
CarouselView takes over the swipe event for scrolling through the items, and Slider does not get the event (DragStarted is not even called). You can actually click along the slider bar to change its value, so it's not completely frozen, but not how it's supposed to work. Drag & drop is main way user deal with slider control.
Could anyone advise any workaround? I want users to be able scroll through carousel view items also. It's just if they swipe inside the control, event should not handed over to its parent container, if it's possible to do so.
If I add it outside of the corouselview, combine both in Grid and use padding to align slider inside the corouselview, it works as expected, but I need to add lots of additional code, calculate the desirable location and redirect all bindings, which ends up to be an awkward workaround.
At first, I don't suggest you use the slider in the CarouselView. Becasue you want the same behavior has two effects. There is a conflict between them.
But for the android, you can use the custom handler to deal with the swipe event.
Put the Handler class in the /Platform/Android:
public class MySliderHander : SliderHandler
{
protected override void ConnectHandler(SeekBar platformView)
{
base.ConnectHandler(platformView);
platformView.SetOnTouchListener(new SliderListener());
// the listener will make the slider deal with the swip event not the CarouselView.
}
}
Put the SliderListener class in the /Platform/Android
public class SliderListener : Java.Lang.Object, IOnTouchListener
{
public bool OnTouch(global::Android.Views.View v, MotionEvent e)
{
if (e.Action == MotionEventActions.Down || e.Action == MotionEventActions.Move)
{
v.Parent.RequestDisallowInterceptTouchEvent(true);
}
else
{
v.Parent.RequestDisallowInterceptTouchEvent(false);
}
return false;
}
}
And in the MauiProgram.cs:
builder
.UseMauiApp<App>()
.ConfigureMauiHandlers(handlers => {
#if ANDROID
handlers.AddHandler(typeof(Slider), typeof(YourProjectName.Platforms.Android.MySliderHander));
#endif
})
In addition, the Slider's height is same as the CarouselView. So you can use a frame to contain the Slider and swipe the CarouselView by swiping the frame.
I have a really odd problem with variable scopes. A Listview named "TodoListView" is defined via xaml, and it's ItemSource populated from a SQListe database. Works. Inside the ListView I have a ViewCell to display the data row-wise.
<ContentPage ... x:Class="JanitorPro.MainPage" ...>
<StackLayout>
<ListView x:Name="TodoListView" Margin="20" ItemSelected="OnListItemSelected">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Horizontal" HorizontalOptions="FillAndExpand">
<Label Text="{Binding name}" VerticalTextAlignment="Center" HorizontalOptions="StartAndExpand" />
<Switch HorizontalOptions="End" IsToggled="{Binding done}" Toggled="DoneSwitchToggled"/>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
The codebehind looks like this (some irrelevant portions removed):
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
}
protected override async void OnAppearing()
{
base.OnAppearing();
// load Database
TodoListView.ItemsSource = await App.TodoDatabase.GetItemsAsync("SELECT * FROM [TodoItem]");
}
async void OnReloadButtonClicked(object sender, EventArgs e)
{
Debug.WriteLine("Reload Button Click");
TodoListView.ItemsSource = await App.TodoDatabase.GetItemsAsync("SELECT * FROM [TodoItem]");
Debug.WriteLine("Reload done.");
}
async void OnListItemSelected(object sender, SelectedItemChangedEventArgs e)
{
if (e.SelectedItem != null)
{
await Navigation.PushAsync(new TodoItemPage
{
BindingContext = e.SelectedItem as TodoItem
});
}
}
private void DoneSwitchToggled(object sender, ToggledEventArgs e)
{
// TodoItem i = null;
TodoItem i = TodoListView.SelectedItem;
if (i != null)
{
Debug.WriteLine("Toggle: {0}", i.id);
}
}
}
}
The oddity has two stages. Before I inserted the DoneSwitchToggled event handler, every occurrance of TodoListView.ItemsSource got a red underline under TodoListView and a hint that "The name TodoListView does not exist in the current context". OK, I thought that VS was not smart enough to find a definition in the xaml file, because, despite of the warning, the program compiled and ran fine. TodoListView gets initialized and does correctly display the rows of the underlying database, so it does clearly exist at runtime.
Things went wild when I added the DoneSwitchToggled event handler to both XAML and the codebehind. All the sudden the program won't compile any longer but bail out with a CS0103 error "The name "TodoListView" does not exist in the current context". The error appears three times, with the line numbers pointing to the other occurrances of TodoListView in onAppearing() and OnReloadButtonClicked(). Huh? How the heck can the addition of a variable reference in an event handler render that variable invalid in completely different methods? OK, there was something fishy with the variable before (still don't know what ...), but it worked. Now it doesn't any more, whch doesn't make any sense for me. Furthermore, if I comment out the offending line in the DoneSwitchToggled event handler, and insert a dummy definition for i, like so:
TodoItem i = null;
// TodoItem i = TodoListView.SelectedItem;
everything is like before, VS still underlines the other appearances of TodoListView, but now the program builds and runs ok again.
Anyone who can explain this effect, and show me how correct my code? I think the objective is clear: DoneSwitchToggled is supposed to write back the switch value into the database (and do some other processing not shown in my stripped down sample), and though the "sender" object is correctly set to reference my button, I found no way to access the underlying data binding, since ToggledEventArgs unfortunately does seem to only pass the switch position "true" or "false", but - unlike the OnListItemSelected event handler - not pass any reference to the bound row through the second argument. So my idea was to use ListView.SelectedItem for this purpose.
Finally I figured it out myself. This seems to be a glitch in VS 2017. There is nothing wrong with TodoListView, so error CS0103 is misleading nonsense.
What VS really means is an error CS0266. TodoListView is defined by a generic list
List<TodoItem>
to access SelectedItem i need to typecast it:
TodoItem i = (TodoItem)TodoListView.SelectedItem;
This added, all errors are gone, app code builds OK.
Btw, unfortunately this approach to get at the item where the Switch has been flipped has proven not working, TodoListView does always return null for SelectedItem, seems that the ListView control doesn't see the button press. Need to find a different way to find the list item beneath the switch to get at the bound row id.
How to automatically raise event in windows phone? For example, I have an element <Image name = "image" .... />. I want when a MainPage is loaded, it will automatically raise tap event on that element
If you want to declare tap event dynamically (loads tap event on page load), You can declare it in following way. Here is your xaml.
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<Image Name="image1"/>
</Grid>
And in constructor,
public MainPage()
{
InitializeComponent();
image1.Tap += image1_Tap;
}
void image1_Tap(object sender, System.Windows.Input.GestureEventArgs e)
{
//Perform your action here
//This method invokes only when you tap on image
}
Else, try the other way.
Loaded += (s, e) =>
{
//Actions that are performed when image is tapped
}
Add above lines in your constructor.
This probably is not a good solution to whatever you are trying to accomplish. It can be done with reflection - How to manually invoke an event? However I'd just extract your Tap event code to method and then call this method in your Loaded event and also Tap even.
I am using a pivot page in my wp7 application. The PivotItems are added programatically. I need to get events of all the gestures. How can I get them?
And, how to know the direction of flick gesture? After swiping how to get details of the current item.
I was trying this : WP7: Questions regarding Touch Gestures. But could not add
<toolkit:GestureService.GestureListener>
<toolkit:GestureListener Flick="GestureListener_Flick" />
</toolkit:GestureService.GestureListener>
when I am trying to add this, an error occurs.
How can I get gesture events?
There is additional support for detecting touch in the XNA library. Trying adding the Microsoft.Xna.Framework.Input.Touch reference to your project
Include the following using statement:
using Microsoft.Xna.Framework.Input.Touch;
Subscribe to the required events in your constructor as follows:
TouchPanel.EnabledGestures = GestureType.Tap | GestureType.Flick;
On your control create an event for Manipulation Completed as follows:
ManipulationCompleted="Object_ManipulationCompleted"
You could add code to the that event method to track the type of events that have been completed with the following code:
private void Object_ManipulationCompleted(object sender, ManipulationCompletedEventArgs e)
{
while (TouchPanel.IsGestureAvailable)
{
GestureSample gesture = TouchPanel.ReadGesture();
if (gesture.GestureType == GestureType.Tap)
{
//Do something
}
if (gesture.GestureType == GestureType.Flick)
{
//The delta contains the direction of the flick (negative/positive)
//gesture.Delta.Y;
//gesture.Delta.X;
}
}
}
Hope this Helps
I've set up a viewmodel to bind a listcontrol to an ObservableCollection in my program. A UI control on the page adds and deletes objects to the collection, which works fine as the list is automatically updated.
After App-Switching and returning to the app, the buttons adds the objects, but the bindings seem to be lost. Any idea how I can maintain this even after returning? I don't really see the need to rebind the object (after defining it in XAML). Is there any way to foolproof this pattern, and ensure the bindings aren't lost upon returning to the app?
The XAML looks like this, but it's inside a UserControl - forgot to mention that
ItemsControl x:Name="PartyCollection" ItemTemplate="{StaticResource PartyCollectiontemplate}" ItemsSource="{Binding RoomParty, Source={StaticResource FormControlVM}}"
the codebehind looks like this
public class FormControlVM : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<Party> RoomParty
{
get
{
return App.appData.currentChoices.roomParty;
}
set
{
App.appData.currentChoices.roomParty = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("RoomParty"));
}
}
}