I'm working on WP8.1 and I've found strage behaviour of my Listbox - to make it easier to reproduce I've put the code here. The problem occurs both on device and emulator.
I've a Listbox which is bound to ObservableCollection and it is filled with items upon button click:
public sealed partial class MainPage : Page
{
List<string> populationOfItems = new List<string>();
ObservableCollection<string> itemsToView = new ObservableCollection<string>();
public MainPage()
{
this.InitializeComponent();
second.Click += second_Click;
myList.ItemsSource = itemsToView;
// populate list of items to be copied
for (int i = 0; i < 6; i++) populationOfItems.Add("Item " + i.ToString());
BottomAppBar = new CommandBar();
}
private async void second_Click(object sender, RoutedEventArgs e)
{
itemsToView.Clear();
await PopulateList();
}
private async Task PopulateList()
{
await Task.Delay(100); // without this line code seems to work ok
itemsToView.Add("FIRSTELEMENT"); // add first differet element
foreach (var item in populationOfItems)
itemsToView.Add(item);
}
}
The first time I fill the list, everthing is ok (pic. 1). But when I hit the button second time and, I can see elements not from first but from second (pic. 2). Ok - it is above, but I'm anable to scroll to it, when I hold my finger (mouse) I can scroll list and see that it exists, but when I stop scrolling the list hides (scrolls to second element) the first element. Also when you select any item - the list seems to look ok while you hold your finger (pic. 3), when you release it hides the first element again. When you move the list few time up/down, it repairs and works normal.
The way to reproduce the issue:
click the scond button once - fill the list
scroll list down, so that you hide the first elements (this is important)
hit the second button once more
try to scroll the list up and see first element, release finger
Pic.1
Pic.2
Pic.3
The problem seems to be concerned with asynchronous Task (that is why I also tagged this question asynchronous) - without the line await Task.Delay(100);, the code seems to work ok.
Does anybody have an idea what can be wrong?
EDIT - some other attempts
I've also tried running populating process via Dispatcher, without success - the problem exists.
I've also made an attempt to populate a temporary List (not ObservableCollection) and after returning from async Task, populate ObservableCollection - the problem persists.
List<string> temporaryList = new List<string>();
private async Task PopulateList()
{
await Task.Delay(100); // without this line code seems to work ok
temporaryList.Clear();
temporaryList.Add("FIRSTELEMENT"); // add first differet element
foreach (var item in populationOfItems)
temporaryList.Add(item);
}
private async void second_Click(object sender, RoutedEventArgs e)
{
itemsToView.Clear();
await PopulateList();
foreach (var item in temporaryList)
itemsToView.Add(item);
}
Edit 2 - returnig List created in acync Task also doesn't help much:
private async void second_Click(object sender, RoutedEventArgs e)
{
itemsToView.Clear();
var items = await PopulateList();
foreach (var item in items)
itemsToView.Add(item);
}
private async Task<IEnumerable<string>> PopulateList()
{
await Task.Delay(100); // without this line code seems to work ok
List<string> temporaryList = new List<string>();
temporaryList.Add("FIRSTELEMENT"); // add first differet element
foreach (var item in populationOfItems)
temporaryList.Add(item);
return temporaryList;
}
EDIT 3 - as I've checked the same code run under Windows Phone 8.1 Silverlight works without problems.
You should not mix UI handling with data retrieval because they are now happening concurrently.
Be also aware that when you call await PopulateList() the execution flow goes back to the UI thread and is ready to accept clicks and fire click events.
Try this instead:
private async void second_Click(object sender, RoutedEventArgs e)
{
// UI thread
var items = await PopulateListAsync(); // -> return to UI thread
// back to UI thread
itemsToView.Clear();
itemsToView.Add("FIRSTELEMENT"); // add first differet element
foreach (var item in items)
{
itemsToView.Add(item);
}
}
private async Task<IEnumerable<string>> PopulateListAsync()
{
// caller thread - UI thread
await Task.Delay(100)
.ConfigureAwait(continueOnCapturedContext: false);
// some other thread
return populationOfItems;
}
You might want to read this:
Asynchronous Programming with Async and Await (C# and Visual Basic)
Task-based Asynchronous Pattern (TAP)
Best Practices in Asynchronous Programming
EDIT:
I believe this demonstrates what you're trying to do. I've added a few more delays for you to see it happening on the phone.
MainPage.xaml
<phone:PhoneApplicationPage
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:System="clr-namespace:System;assembly=mscorlib"
x:Class="PhoneApp1.MainPage"
mc:Ignorable="d"
FontFamily="{StaticResource PhoneFontFamilyNormal}"
FontSize="{StaticResource PhoneFontSizeNormal}"
Foreground="{StaticResource PhoneForegroundBrush}"
SupportedOrientations="Portrait" Orientation="Portrait"
shell:SystemTray.IsVisible="True">
<Grid x:Name="LayoutRoot" Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
<TextBlock Text="MY APPLICATION" Style="{StaticResource PhoneTextNormalStyle}" Margin="12,0"/>
<TextBlock Text="page name" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/>
</StackPanel>
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<phone:LongListSelector x:Name="List" HorizontalAlignment="Stretch" Margin="0" VerticalAlignment="Stretch" ItemsSource="{Binding}" LayoutMode="List">
<phone:LongListSelector.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" Style="{StaticResource PhoneTextExtraLargeStyle}" />
</DataTemplate>
</phone:LongListSelector.ItemTemplate>
</phone:LongListSelector>
<Button Content="Button" Margin="0" Grid.Row="1" Click="Button_Click" x:Name="Button1"/>
</Grid>
</Grid>
</phone:PhoneApplicationPage>
MainPage.xaml.cs
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Windows;
using Microsoft.Phone.Controls;
namespace PhoneApp1
{
public partial class MainPage : PhoneApplicationPage
{
private List<string> populationOfItems = new List<string>
{
"one",
"two",
"three",
"four",
"five"
};
private ObservableCollection<string> itemsToView = new ObservableCollection<string>();
public MainPage()
{
InitializeComponent();
this.DataContext = this.itemsToView;
}
private async void Button_Click(object sender, RoutedEventArgs e)
{
this.Button1.IsEnabled = false;
var items = await PopulateListAsync();
itemsToView.Clear();
await Task.Delay(100);
itemsToView.Add("FIRSTELEMENT");
foreach (var item in items)
{
await Task.Delay(10);
itemsToView.Add(item);
}
this.Button1.IsEnabled = true;
}
private async Task<IEnumerable<string>> PopulateListAsync()
{
await Task.Delay(100)
.ConfigureAwait(continueOnCapturedContext: false);
return populationOfItems;
}
}
}
Related
I have been trying to implement a feature into my application where when a user hovers over a Grid item housed within a GridView for a couple seconds it will display a medium sized popup Grid with more details. I have tried a few methods I found online, but I have been running into errors that I am not understanding the root cause of, and I have the feeling there is a much simpler approach to add this feature than what I have found online so far.
First, I tried a solution I adapted by using a derivative class of Grid. I ran into some issues with that that (detailed a bit more in my last question) with the main issue being that my Timer no longer would trigger and items using my "HoverGrid" data template within the GridView template would no longer show Image as a child item of the HoverGrid. So, I abandoned that approach.
Then I tried to implement the Timer directly in my Page's code-behind, which seemed to work (partially) as it is properly triggering PointerEnter, PointerExit, and TimerElapsed events, however, when trying to manipulate anything UI related in the TimerElapsed event I would get:
System.Exception: 'The application called an interface that was marshalled for a different thread. (Exception from HRESULT: 0x8001010E (RPC_E_WRONG_THREAD))'
Here is the XAML:
<Page
x:Class="myproject.Pages.MyPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.Toolkit.Uwp.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Height="950"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
mc:Ignorable="d">
<Grid x:Name="ContentGrid">
<ListView
x:Name="AreaPanels"
Margin="0,10,0,0"
HorizontalContentAlignment="Stretch"
SelectionMode="None">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<ItemsStackPanel AreStickyGroupHeadersEnabled="True" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<controls:AdaptiveGridView
CanDragItems="True"
DesiredWidth="140"
ItemHeight="140">
<controls:AdaptiveGridView.ItemTemplate>
<DataTemplate x:Name="IconTextTemplate" x:DataType="Image">
<Grid
PointerEntered="ImageHoverStart"
PointerExited="ImageHoverEnd">
<Image
Opacity="1"
Source="/Assets/Placeholders/sample_image.jpg"
Stretch="UniformToFill" />
</Grid>
</DataTemplate>
</controls:AdaptiveGridView.ItemTemplate>
<Grid />
<Grid />
<Grid />
<Grid />
</controls:AdaptiveGridView>
</ListView>
</Grid>
</Page>
Code-behind (C#):
using System.Diagnostics;
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
namespace myproject.Pages
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MyPage : Page
{
//Variables for Grid functions
public object hoveredGridItem = null;
public PointerRoutedEventArgs hoveredGridItemArgs = null;
public System.Timers.Timer hoverTimer = new System.Timers.Timer();
public event System.Timers.ElapsedEventHandler TimerElapsed
{ add { hoverTimer.Elapsed += value; } remove { hoverTimer.Elapsed -= value; } }
public MyPage()
{
this.InitializeComponent();
hoverTimer.Enabled = true;
hoverTimer.Interval = 2000;
TimerElapsed += OnTimerElapsed;
}
private void ImageHoverStart(object sender, PointerRoutedEventArgs e)
{
Debug.WriteLine("Fired: ImageHoverStart");
hoverTimer.Start();
hoveredGridItem = sender;
hoveredGridItemArgs = e;
}
private void ImageHoverEnd(object sender, PointerRoutedEventArgs e)
{
Debug.WriteLine("Fired: ImageHoverEnd");
hoverTimer.Stop();
hoveredGridItem = null;
hoveredGridItemArgs = null;
}
private void OnTimerElapsed(object source, System.Timers.ElapsedEventArgs e)
{
Debug.WriteLine("Timer elapsed!");
hoverTimer.Stop();
if (hoveredGridItem.GetType().ToString() == "Windows.UI.Xaml.Controls.Grid")
{
Debug.WriteLine(hoveredGridItem.ToString());
Debug.WriteLine(hoveredGridItemArgs.ToString());
//Get the hovered image and associated arguments that were stored
Grid itm = (Grid)hoveredGridItem;
PointerRoutedEventArgs f = hoveredGridItemArgs;
//Get image position and bounds
//GeneralTransform transform = itm.TransformToVisual(Window.Current.Content);
//Point coordinatePointToWindow = transform.TransformPoint(new Point(0, 0));
//Rect winBounds = Window.Current.Bounds;
//Testing other UI items
itm.Visibility = Visibility.Visible;
// other UI stuff ...
}
}
}
}
I tried to make references to some UI elements such as Window.Content.Current and other elements (as a workaround) but was still getting the same System.Exception. I understand this has something to do with TimerElapsed being on a different thread than the UI thread and looked around for how to fix this but was not able to fix it.
My two issues are that I was not able to fix the thread marshalling issue (some issues with running things async) but more importantly that the solution seemed a bit convoluted, maybe more difficult than it needs to be.
At first to fix the threading issue you have to use the Dispatcher of the UIElement:
await itm.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
itm.Visibility = Visibility.Visible;
});
And second, have you thought about using a tooltip for this? It should be very easy to implement:
<Grid Background="Transparent">
<Image Opacity="1" Source="/Assets/StoreLogo.png" Stretch="UniformToFill" />
<!--Tooltip implementation:-->
<ToolTipService.ToolTip>
<Image Source="/Assets/StoreLogo.png"/>
</ToolTipService.ToolTip>
</Grid>
I'm trying to implement Drag & Drop with files on a ListBox which is contained in a Window of an Avalonia project.
As I couldn't get it working and I thought that ListBox perhaps is a special case, I tried to make a similar example like the one from ControlCatalogStandalone.
While the code in ControlCatalogStandalone works as expected, virtually the same code in my test application doesn't work properly.
The relevant code in ControlCatalogStandalone belongs to a UserControl, where in my application it belongs to the MainWindow. Could this be the cause for the misbehavior?
I created a new Avalonia MVVM Application based on the NuGet packages 0.9.11 in Visual Studio 2019.
I also tried version 0.10.0-preview2 in vain.
This is the XAML file:
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:DragAndDropTests.ViewModels;assembly=DragAndDropTests"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" Width="400" Height="200"
x:Class="DragAndDropTests.Views.MainWindow"
Icon="/Assets/avalonia-logo.ico"
Title="DragAndDropTests">
<Design.DataContext>
<vm:MainWindowViewModel/>
</Design.DataContext>
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">Drag+Drop</TextBlock>
<TextBlock Classes="h2">Example of Drag+Drop capabilities</TextBlock>
<StackPanel Orientation="Horizontal"
Margin="0,16,0,0"
HorizontalAlignment="Center"
Spacing="16">
<Border BorderBrush="{DynamicResource ThemeAccentBrush}" BorderThickness="2" Padding="16" Name="DragMe">
<TextBlock Name="DragState">Drag Me</TextBlock>
</Border>
<Border Background="{DynamicResource ThemeAccentBrush2}" Padding="16"
DragDrop.AllowDrop="True">
<TextBlock Name="DropState">Drop some text or files here</TextBlock>
</Border>
</StackPanel>
</StackPanel>
</Window>
And this is the Code Behind:
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using System;
using System.Diagnostics;
namespace DragAndDropTests.Views
{
public class MainWindow : Window
{
private TextBlock _DropState;
private TextBlock _DragState;
private Border _DragMe;
private int DragCount = 0;
public MainWindow()
{
Debug.WriteLine("MainWindow");
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
_DragMe.PointerPressed += DoDrag;
AddHandler(DragDrop.DropEvent, Drop);
AddHandler(DragDrop.DragOverEvent, DragOver);
}
private async void DoDrag(object sender, Avalonia.Input.PointerPressedEventArgs e)
{
Debug.WriteLine("DoDrag");
DataObject dragData = new DataObject();
dragData.Set(DataFormats.Text, $"You have dragged text {++DragCount} times");
var result = await DragDrop.DoDragDrop(e, dragData, DragDropEffects.Copy);
switch (result)
{
case DragDropEffects.Copy:
_DragState.Text = "The text was copied"; break;
case DragDropEffects.Link:
_DragState.Text = "The text was linked"; break;
case DragDropEffects.None:
_DragState.Text = "The drag operation was canceled"; break;
}
}
private void DragOver(object sender, DragEventArgs e)
{
Debug.WriteLine("DragOver");
// Only allow Copy or Link as Drop Operations.
e.DragEffects = e.DragEffects & (DragDropEffects.Copy | DragDropEffects.Link);
// Only allow if the dragged data contains text or filenames.
if (!e.Data.Contains(DataFormats.Text) && !e.Data.Contains(DataFormats.FileNames))
e.DragEffects = DragDropEffects.None;
}
private void Drop(object sender, DragEventArgs e)
{
Debug.WriteLine("Drop");
if (e.Data.Contains(DataFormats.Text))
_DropState.Text = e.Data.GetText();
else if (e.Data.Contains(DataFormats.FileNames))
_DropState.Text = string.Join(Environment.NewLine, e.Data.GetFileNames());
}
private void InitializeComponent()
{
Debug.WriteLine("InitializeComponent");
AvaloniaXamlLoader.Load(this);
_DropState = this.Find<TextBlock>("DropState");
_DragState = this.Find<TextBlock>("DragState");
_DragMe = this.Find<Border>("DragMe");
}
}
}
Drag & Drop within the application works well in ControlCatalogStandalone and in my application.
The succession of events is DoDrag, DragOver, DragOver, …, Drop in this case.
Dragging a file from Windows Explorer to the ControlCatalogStandalone works well.
The succession of events is DragOver, DragOver, …, Drop
Dragging a file from Windows Explorer to my application doesn't work.
None of the expected events is called here.
What's wrong with my test application?
I implemented a sample application:
You can select the type that is used for the data grid that is then displayed in the dialog. If the user clicks the button, this code will be executed:
private void ShowDialog()
{
Window dialogView = (Window)Activator.CreateInstance(dialogs[selectedDialog]);
dialogView.ShowDialog();
}
Findings:
If “WPF” is selected, the dialog and DataGrid will be displayed immediately.
If “Infragistics” is selected, it takes more than a second for displaying the dialog and XamDataGrid.
If the “Infragistics” dialog is opened a second time, it will show up much faster.
Then I started profiling and clicked the button for “WPF” and two times “Infragistics”. Here is the timeline for these three clicks:
The XAML of the “Infragistics” dialog looks like this:
<Grid DataContext="{Binding DataGridDialog, Source={StaticResource Locator}}">
<igDP:XamDataGrid DataSource="{Binding Rows}" Width="300" Height="300"/>
</Grid>
The XAML of the “WPF” dialog looks like this:
<Grid DataContext="{Binding DataGridDialog, Source={StaticResource Locator}}">
<DataGrid ItemsSource="{Binding Rows}" Width="300" Height="300"/>
</Grid>
Anyway, the gap between the button click and the “Infragistics” dialog being displayed the very first time is not acceptable for a user. That is why I wrote the following code in the code behind for the "Infragistics" dialog that enables a busy indicator between the events “Initialized” and “Loaded”. Unfortunately the busy indicator's animation is not responsive:
public partial class InfragisticsDataGridDialogView : Window
{
private IUserInteractionService userInteractionService;
private TaskCompletionSource<object> tcs;
public InfragisticsDataGridDialogView(IUserInteractionService userInteractionService)
{
this.userInteractionService = userInteractionService;
Loaded += OnLoaded;
Initialized += OnInitialized;
InitializeComponent();
}
private async void OnInitialized(object sender, EventArgs e)
{
tcs = new TaskCompletionSource<object>();
await ShowBusyIndicatorAsync(tcs.Task);
}
private async Task ShowBusyIndicatorAsync(Task task)
{
await userInteractionService.ShowBusyIndication("Opening dialog", task);
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
tcs.SetResult(null);
}
}
Is there a way to make the busy indicator responsive? The busy indicator is a Grid in the main Window:
<Grid Visibility="{Binding UserInteractionService.ShowBusyIndicator, Converter={StaticResource BoolToVisibilityConverter}}">
<ProgressBar APProgressBar.SubTitle="{Binding UserInteractionService.BusyMessage}" IsIndeterminate="True" />
</Grid>
So, lets say I have STA thread running on background and I create a user control there.
How functional it is going to be? What are the limitations?
_workingThread = new Thread(() =>
{
//so far so good
var myControl = new MyCustomControl();
//what happens if i set DataContext? Will databinding work?
//It looks like it does, but I am not entirely sure.
myControl.DataContext = new MyViewModel();
//if databinding works, can I assume that at this point
//myControl's properties are already updated?
//what happens exactly if I invoke a delgate using Dispatcher property?
myControl.Dispatcher.Invoke(SomeMethod);
//or current dispatcher?
Dispatcher.CurrentDispatcher.BeginInvoke(SomeOtherMethod);
});
_workingThread.SetApartmentState(ApartmentState.STA);
_workingThread.Start();
To answer the question why: there is a component in .Net called XpsDocument which allows you to write visuals into xps file. I don't see a reason, why I should do it on UI thread.
Here is example of WPF app, which creates Window in new STA Thread. I don't see any problem with it. I printed out some things: Thread name, ThreadId and Counter (changes via INotifyPropertyChanged). Also I change stackPanelCounter's background from timer Dispatcher.BeginInvoke.
XAML:
<Window x:Class="WpfWindowInAnotherThread.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"
mc:Ignorable="d"
SizeToContent="WidthAndHeight" WindowStartupLocation="CenterScreen" Title="WPF: Windows and Threads">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto"/>
<RowDefinition />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Vertical">
<TextBlock Text="{Binding ThreadId, StringFormat='ThreadId: {0}'}" />
<TextBlock Text="{Binding ThreadName, StringFormat='ThreadName: {0}'}" />
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal" Name="stackPanelCounter">
<TextBlock Text="Counter: " />
<TextBlock Text="{Binding Counter}" />
</StackPanel>
<StackPanel Grid.Row="2">
<Button Name="btnStartInNewThread" Content="Start window in new Thread"
Click="btnStartInNewThread_Click"/>
<Button Name="btnStartTheSameThread"
Content="Start window in the same Thread"
Click="btnStartTheSameThread_Click" />
</StackPanel>
</Grid>
</Window>
Code:
using System;
using System.ComponentModel;
using System.Threading;
using System.Windows;
using System.Windows.Media;
using System.Windows.Threading;
namespace WpfWindowInAnotherThread
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window, INotifyPropertyChanged
{
static int _threadNumber = 0;
readonly Timer _timer;
int _Counter;
public event PropertyChangedEventHandler PropertyChanged = delegate { };
public int ThreadId
{
get { return Thread.CurrentThread.ManagedThreadId; }
}
public string ThreadName
{
get { return Thread.CurrentThread.Name; }
}
public int Counter
{
get { return _Counter; }
set { _Counter = value; PropertyChanged(this, new PropertyChangedEventArgs("Counter")); }
}
public MainWindow()
{
DataContext = this;
_timer = new Timer((o) => {
Counter++;
MainWindow wnd = o as MainWindow;
wnd.Dispatcher.BeginInvoke(new Action<MainWindow>(ChangeStackPanelBackground), wnd);
}, this, 0, 200);
InitializeComponent();
}
private void btnStartTheSameThread_Click(object sender, RoutedEventArgs e)
{
MainWindow mainWnd = new MainWindow();
mainWnd.Show();
}
private void btnStartInNewThread_Click(object sender, RoutedEventArgs e)
{
Thread thread = new Thread(new ThreadStart(ThreadMethod));
thread.SetApartmentState(ApartmentState.STA);
thread.IsBackground = true;
thread.Start();
}
private static void ThreadMethod()
{
Thread.CurrentThread.Name = "MainWindowThread# " + _threadNumber.ToString();
Interlocked.Increment(ref _threadNumber);
MainWindow mainWnd = new MainWindow();
mainWnd.Show();
Dispatcher.Run();
}
private static void ChangeStackPanelBackground(MainWindow wnd)
{
Random rnd = new Random(Environment.TickCount);
byte[] rgb = new byte[3];
rnd.NextBytes(rgb);
wnd.stackPanelCounter.Background = new SolidColorBrush(Color.FromArgb(0xFF, rgb[0], rgb[1], rgb[2]));
}
}
}
I spent some time testing things out, and I think Clemens's comment was accurate. Key points are:
myControl.Dispatcher and Dispatcher.CurrentDispatcher are one and the same, both hold a reference to dispatcher of background thread. No surprises here.
In general controls will not behave correctly without dispatcher running, because Dispatcher.BeginInvoke calls will not be processed. You have two options. Either call Dispatcher.Run() on background thread and create your controls using invokes:
_backgroundDispatcher.BeginInvoke(new Action(() =>
{
var myControl = new MyCustomControl();
//do stuff
}));
or manually push dispatcher frame every time you want to process dispatcher queue and "refresh" your control. When it comes to building XPS pages, both approaches are viable.
Data bindings do work, even when control is created on background thread. However in some cases they are not applied instantly and you might have to wait for dispatcher to process it's queue.
My team has been struggling for the Best Practices approach for handling the response from a Navigation for about 3 weeks now without a definitive answer. We have both a WPF and a Windows Phone 8 solution where we share a common code base.
For Phone 8, we display our company's splash screen and start initializing our data. Due to our complex nature, we have a very long list of steps to initialize before the application is fully operational.
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if (e.NavigationMode == NavigationMode.New)
{
BeginAppStartup();
return;
}
....
void BeginAppStartup()
{
// Initialization of settings and environment
At this point, we need to optionally display up to 5 different pages requesting additional data. So we check our commands, and if executable, then we navigate and optionally display a Communication Page, a Login Page, or several other possible pages.
if( condition )
DisplayLoginPage();
In WPF, this would be easy since we have modal dialogs and can wait for the user's input before continuing. But in the asynchronous world of WP8, we no longer have this.
To accommodate this platform, we have implemented a wide array of attempts, including saving the next command to execute. The only place that I believe that we are assured that the page is closed is in the OnNavigatedTo of the splash page.
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if (e.NavigationMode == NavigationMode.Back)
{
// If we are returning to the splash from another set up page, check if there are new actions to perform
if (_startupAction != null)
{
_startupAction();
return;
}
Unfortunately, this is only marginally acceptable since the Login page doesn't close properly since all of our action is in the UI thread. The code continues, but the splash page is hidden behind the still visible Login page.
We have also tried out AutoResetEvents, but since we must Navigate out of the UI thread, we can't block the UI thread. We've also tried Task.Run with similar issues.
// Doesn't work.
void ShowLoginPage()
{
if (condition)
{
_manualResetEvent.Reset();
NavigationService.Navigate(new Uri("/Views/Login.xaml", UriKind.Relative)
_manualResetEvent.WaitOne();
}
}
We've also tried the async/await tasks, but we encounter similar problems. I believe that this is the best solution, but we're not having any better luck than previously.
So back to the question: What is the Best Practice for Navigating from a splash page, optionally to a login page, and then to await for the login page to close completely before continuing?
This sounds like a very common scenario, yet I'm baffled! Thanks for your answers.
It is not difficult to provide a functionality similar to a modal dialog. I'm not sure if it is a great UI design decision, but it certainly can be done. This MSDN blog post describes how to do it with UserControl as a custom adorner. It was written in 2007, by that time there was no async/await nor WP8.
I'm going to show how to do a similar thing using Popup control (which is present in both WPF and WP8) and async/await. Here's the functional part:
private async void OpenExecuted(object sender, ExecutedRoutedEventArgs e)
{
await ShowPopup(this.firstPopup);
await ShowPopup(this.secondPopup);
}
Each popup can and should be data-bound to the ViewModel.
C# (a WPF app):
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
namespace Wpf_22297935
{
public partial class MainWindow : Window
{
// http://stackoverflow.com/q/22297935/1768303
public MainWindow()
{
InitializeComponent();
}
EventHandler ProcessClosePopup = delegate { };
private void CloseExecuted(object sender, ExecutedRoutedEventArgs e)
{
this.ProcessClosePopup(this, EventArgs.Empty);
}
// show two popups with modal-like UI flow
private async void OpenExecuted(object sender, ExecutedRoutedEventArgs e)
{
await ShowPopup(this.firstPopup);
await ShowPopup(this.secondPopup);
}
private void CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
// helpers
async Task ShowPopup(Popup popup)
{
var tcs = new TaskCompletionSource<bool>();
EventHandler handler = (s, e) => tcs.TrySetResult(true);
this.ProcessClosePopup += handler;
try
{
EnableControls(false);
popup.IsEnabled = true;
popup.IsOpen = true;
await tcs.Task;
}
finally
{
EnableControls(true);
popup.IsOpen = false;
popup.IsEnabled = false;
this.ProcessClosePopup -= handler;
}
}
void EnableControls(bool enable)
{
// assume the root is a Panel control
var rootPanel = (Panel)this.Content;
foreach (var item in rootPanel.Children.Cast<UIElement>())
item.IsEnabled = enable;
}
}
}
XAML:
<Window x:Class="Wpf_22297935.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Window.CommandBindings>
<CommandBinding Command="ApplicationCommands.Open" CanExecute="CanExecute" Executed="OpenExecuted" />
<CommandBinding Command="ApplicationCommands.Close" CanExecute="CanExecute" Executed="CloseExecuted"/>
</Window.CommandBindings>
<DockPanel>
<Border Padding="5">
<StackPanel>
<StackPanel>
<TextBlock>Main:</TextBlock>
<TextBox Height="20"></TextBox>
<Button Command="ApplicationCommands.Open" HorizontalAlignment="Left" Width="50">Open</Button>
</StackPanel>
<Popup Name="firstPopup" AllowsTransparency="true" Placement="Center">
<Border Background="DarkCyan" Padding="5">
<StackPanel Background="DarkCyan" Width="200" Height="200" HorizontalAlignment="Left">
<TextBlock>First:</TextBlock>
<TextBox Height="20"></TextBox>
<Button Command="ApplicationCommands.Close" HorizontalAlignment="Left" Width="50">Close</Button>
</StackPanel>
</Border>
</Popup>
<Popup Name="secondPopup" AllowsTransparency="true" Placement="Center">
<Border Background="DarkGray" Padding="5">
<StackPanel Background="DarkGray" Width="200" Height="200" HorizontalAlignment="Left">
<TextBlock>Second:</TextBlock>
<TextBox Height="20"></TextBox>
<Button Command="ApplicationCommands.Close" HorizontalAlignment="Left" Width="50">Close</Button>
</StackPanel>
</Border>
</Popup>
</StackPanel>
</Border>
</DockPanel>
</Window>
When dealing with such complex navigation, you should resort to creating your own navigation service. Instead of using NavigationService.Navigate, use your own wrapper over it.
In case of login page being after splash screen (and optionally), but before some other, you can always remove the page from backstack after navigation. So in this case you always navigate forward to another page and your custom service should remove last page if it is, say, a login page.