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.
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>
I have frames in my main window which are set to visible/collapsed based on user input:
<Grid>
<ScrollViewer x:Name="ScrollViewer1" Grid.Row="1" Grid.ColumnSpan="3" Margin="10,0,0,0" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
<Frame Name="InputsFrame" Source="Inputs.xaml" NavigationUIVisibility="Hidden" Visibility="Visible"
ScrollViewer.CanContentScroll="True" />
</ScrollViewer>
<ScrollViewer x:Name="ScrollViewer2" Grid.Row="1" Grid.ColumnSpan="3" Margin="10,0,0,0" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" Visibility="Collapsed">
<Frame Name="LoadCasesFrame" Source="LoadCases.xaml" NavigationUIVisibility="Hidden" Visibility="Collapsed"
ScrollViewer.CanContentScroll="True" />
</ScrollViewer>
<!-- etc -->
</Grid>
The Inputs.xaml frame basically just consists of a 3rd party DoubleTextBox control (over 100 of them), and the user can just enter in values to that page. C# code behind:
private void InputsTab_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
LoadCasesFrame.Visibility = Visibility.Collapsed;
ScrollViewer2.Visibility = Visibility.Collapsed;
InputsFrame.Visibility = Visibility.Visible;
ScrollViewer1.Visibility = Visibility.Visible;
}
In this main window, there is a menu on top to allow for saving and opening the file. When I open the file, I want the data to be read (which I am able to do successfully) and also for the UI in the Inputs.xaml file to be updated.
The following code is in Inputs.xaml.cs:
public void LoadValues()
{
List<DoubleTextBox> dtb1 = App.GetLogicalChildCollection<DoubleTextBox>(inputsGrid);
for (int i = 0; i < dtb1.Count; i++)
{
foreach (var keyValuePair in App.globalDictionary)
{
var doubleTextBox = dtb1[i] as DoubleTextBox;
if (doubleTextBox.Name == keyValuePair.Key)
{
doubleTextBox.Value = 500;
break;
}
}
}
}
This function works (all the values in the GUI update to 500) when I call it from the Inputs.xml.cs page (for example, when I put it in the Page_Loaded event). However, I need to call this function from the MainWindow, since that is where the event handler for the Open File event is located:
private void openProject_Click(object sender, RoutedEventArgs e)
{
OpenFileDialog openFileDialog = new OpenFileDialog();
if (openFileDialog.ShowDialog() == true)
{
string stringToDeserialize = File.ReadAllText(openFileDialog.FileName);
App.DeserializeJSONString(stringToDeserialize);
}
// call LoadValues here
}
Calling LoadValues() above doesn't update the GUI in the Input.xaml page. I originally had something like this in my MainWindow:
Inputs _inputs = new Inputs();
_inputs.LoadValues();
I know that the problem is that I have created a new object for Inputs and that's probably why it's not working. I'm unsure how to do it so that I don't use a new object -- wonder if I could use the InputsFrame somehow. I've also tried using event handlers to no success.
If you want to interact with the Inputs object that is in your Frame element, you need to retrieve that one, not create a new one.
You can do that like this:
((Inputs)InputsFrame.Content).LoadValues();
I.e. the Contents property of the Frame returns a reference to the Inputs object that is used to populate the Frame. Just cast that to Inputs to access the appropriate members of that class, such as LoadValues().
My application is using an image processing library to handle a long running task. The main UI with settings and controls is implemented in WPF. The image processing needs to be displayed and the main UI needs to remain responsive. Upon clicking the 'process' button in the main UI a new thread is spawned which creates a new WinForm window to display the processed images in.
Before it was multithreaded the UI would hang while processing and the progress would be visible in the WinForm for displaying the images. Then when the processing would complete the WinForm would remain with the image in it. Events are added to the new WinForm that allow panning and zooming. The panning and zooming functionality worked correctly.
It became obvious due to the requirements of the project that it would need to be multithreaded to function properly.
Now with the new thread the WinForm window is created as before and the image is processed and displayed. The problem is that when this method is completed the thread exits. Having the thread exit means that if the allocated image buffers are not freed then the application throws an exception. To fix this there is a method called to free all allocations before the thread exits. This fixes the exception and makes the entire thread execute successfully but it means that the image display buffer and form to display it in are freed/disposed of and so there is not time available for the zooming and panning events.
The best solution to make the Thread not exit was to make an AutoResetEvent and have something like this at the end of the image processing thread.
while (!resetEvent.WaitOne(0, false)) { }
threadKill(); // frees all allocations
The AutoResetEvent is fired by the by a button on the main UI that kills the thread. This works to have the image display as long as needed and killed explicitly by the user, however it fails to allow the firing of Click and Drag events needed to make the image pan and zoom. Is there a way to make the thread not exit without having a spinning while loop which prevents the events from being fired? The desired functionality is to have the thread remain alive so that the allocations do not have to be freed and the panning and zooming can be implemented.
Even though the solution may be obvious to someone with more experience threading, any help would be appreciated as I am new to multithreaded applications.
Thanks
EDIT: It should be known that the end goal is to display a constant stream of frames which are processed in this way taken from a frame grabber. So I don't think that it will work to process them separately in the background and then display them in the main UI, because there is going to need to be a constant stream of displays and this would lock up the main UI.
EDIT: The real intent of the question is not to find a better way to do something similar. Instead I am asking if the new thread can be stopped from exiting so that the click events can fire. If this behavior cannot be achieved with System.Threading.Thread then saying it cannot be achieved would also be an accepted answer.
If you can use the new parallel classes and collections in C# 4.0 this is a pretty easy task. Using a BlockingCollection<T> you can add images from any thread to the collection and have a background consumer taking images off this collection and process them. This background processing can be easily created and managed (or canceled) using a Task from the TaskFactory. Check out this simple WPF application for loading images and converting them to black and white as long as there are images to process without blocking the UI. It doesn't use two windows but I think it demonstrates the concepts:
using System;
using System.Collections.Concurrent;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
using Microsoft.Win32;
namespace BackgroundProcessing
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window, INotifyPropertyChanged
{
private readonly BlockingCollection<BitmapImage> _blockingCollection = new BlockingCollection<BitmapImage>();
private readonly CancellationTokenSource _tokenSource = new CancellationTokenSource();
private ImageSource _processedImage;
public MainWindow()
{
InitializeComponent();
CancellationToken cancelToken = _tokenSource.Token;
Task.Factory.StartNew(() => ProcessBitmaps(cancelToken), cancelToken);
PendingImages = new ObservableCollection<BitmapImage>();
DataContext = this;
}
public ObservableCollection<BitmapImage> PendingImages { get; private set; }
public ImageSource ProcessedImage
{
get { return _processedImage; }
set
{
_processedImage = value;
InvokePropertyChanged(new PropertyChangedEventArgs("ProcessedImage"));
}
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
private void ProcessBitmaps(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
BitmapImage image;
try
{
image = _blockingCollection.Take(token);
}
catch (OperationCanceledException)
{
return;
}
FormatConvertedBitmap grayBitmapSource = ConvertToGrayscale(image);
Dispatcher.BeginInvoke((Action) (() =>
{
ProcessedImage = grayBitmapSource;
PendingImages.Remove(image);
}));
Thread.Sleep(1000);
}
}
private static FormatConvertedBitmap ConvertToGrayscale(BitmapImage image)
{
var grayBitmapSource = new FormatConvertedBitmap();
grayBitmapSource.BeginInit();
grayBitmapSource.Source = image;
grayBitmapSource.DestinationFormat = PixelFormats.Gray32Float;
grayBitmapSource.EndInit();
grayBitmapSource.Freeze();
return grayBitmapSource;
}
protected override void OnClosed(EventArgs e)
{
_tokenSource.Cancel();
base.OnClosed(e);
}
private void BrowseForFile(object sender, RoutedEventArgs e)
{
var dialog = new OpenFileDialog
{
InitialDirectory = "c:\\",
Filter = "Image Files(*.jpg; *.jpeg; *.gif; *.bmp)|*.jpg; *.jpeg; *.gif; *.bmp",
Multiselect = true
};
if (!dialog.ShowDialog().GetValueOrDefault(false)) return;
foreach (string name in dialog.FileNames)
{
CreateBitmapAndAddToProcessingCollection(name);
}
}
private void CreateBitmapAndAddToProcessingCollection(string name)
{
Dispatcher.BeginInvoke((Action)(() =>
{
var uri = new Uri(name);
var image = new BitmapImage(uri);
image.Freeze();
PendingImages.Add(image);
_blockingCollection.Add(image);
}), DispatcherPriority.Background);
}
public void InvokePropertyChanged(PropertyChangedEventArgs e)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, e);
}
}
}
This would be the XAML:
<Window x:Class="BackgroundProcessing.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">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="40"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="3*"/>
</Grid.ColumnDefinitions>
<Border Grid.Row="0" Grid.ColumnSpan="3" Background="#333">
<Button Content="Add Images" Width="100" Margin="5" HorizontalAlignment="Left" Click="BrowseForFile"/>
</Border>
<ScrollViewer VerticalScrollBarVisibility="Visible" Grid.Column="0" Grid.Row="1">
<ItemsControl ItemsSource="{Binding PendingImages}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Image Source="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<Border Grid.Column="1" Grid.Row="1" Background="#DDD">
<Image Source="{Binding ProcessedImage}"/>
</Border>
</Grid>
Use the background worker to process the image for pan and zooming, pass the data to the backgroundworker.RunCompleted Event. You can then display the new image in the main UI thread with no slow down or locking.