ViewControl and ViewForm were not actually reacting when their data context changed -- fixed; view models can now delay their search for a main window (needed to synchronize change notifications to the main threads because of those shitty WinForms controls); WindowManager now accepts ...Page as a postfix for a view name, too

git-svn-id: file:///srv/devel/repo-conversion/nuwi@48 d2e56fa2-650e-0410-a79f-9358c0239efd
This commit is contained in:
Markus Ewald 2019-02-11 18:05:06 +00:00
parent b405fe957b
commit a17926d02e
8 changed files with 171 additions and 52 deletions

View File

@ -59,6 +59,7 @@
<Compile Include="Source\AutoBinding\IAutoBinder.cs" /> <Compile Include="Source\AutoBinding\IAutoBinder.cs" />
<Compile Include="Source\CommonDialogs\CommonDialogManager.cs" /> <Compile Include="Source\CommonDialogs\CommonDialogManager.cs" />
<Compile Include="Source\CommonDialogs\ICommonDialogService.cs" /> <Compile Include="Source\CommonDialogs\ICommonDialogService.cs" />
<Compile Include="Source\LateCheckedSynchronizer.cs" />
<Compile Include="Source\Messages\IMessageService.cs" /> <Compile Include="Source\Messages\IMessageService.cs" />
<Compile Include="Source\Messages\MessageEventArgs.cs" /> <Compile Include="Source\Messages\MessageEventArgs.cs" />
<Compile Include="Source\Messages\MessageEventArgs.Test.cs"> <Compile Include="Source\Messages\MessageEventArgs.Test.cs">
@ -154,7 +155,9 @@
<Link>Foundation.snk</Link> <Link>Foundation.snk</Link>
</None> </None>
</ItemGroup> </ItemGroup>
<ItemGroup /> <ItemGroup>
<Folder Include="Resources\" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets. Other similar extension points exist, see Microsoft.Common.targets.

View File

@ -0,0 +1,137 @@
using System;
using System.ComponentModel;
using System.Windows.Forms;
namespace Nuclex.Windows.Forms {
/// <summary>
/// Proxy stand-in to delay checking for the main window until it has been created
/// </summary>
/// <remarks>
/// <para>
/// The issue: when the view model for the main window is created, the main window
/// may only exist as a .NET object, without the underlying operating system window
/// (done in <see cref="System.Windows.Forms.Control.CreateControl" /> checkable via
/// <see cref="System.Windows.Forms.Control.IsHandleCreated" />). Not only will things
/// like <see cref="System.Windows.Forms.Control.Invoke(Delegate)" /> fail, we can't
/// even locate the main window at that stage.
/// </para>
/// <para>
/// Thus, if the main window cannot be found at the time a view model is created,
/// this late-checking synchronizer will jump into its place and re-check for
/// the main window only when something needs to be executed in the UI thread.
/// </para>
/// </remarks>
class LateCheckedSynchronizer : ISynchronizeInvoke {
/// <summary>Initializes a new late-checked main window synchronizer</summary>
/// <param name="uiContextFoundCallback"></param>
public LateCheckedSynchronizer(Action<ISynchronizeInvoke> uiContextFoundCallback) {
this.uiContextFoundCallback = uiContextFoundCallback;
}
/// <summary>Finds the application's main window</summary>
/// <returns>Main window of the application or null if none has been created</returns>
/// <remarks>
/// The application's main window, if it has been created yet
/// </remarks>
public static Form GetMainWindow() {
IntPtr mainWindowHandle = System.Diagnostics.Process.GetCurrentProcess().MainWindowHandle;
// We can get two things: a list of all open windows and the handle of
// the window that the process has registered as main window. Use the latter
// to pick the correct window from the former.
FormCollection openForms = Application.OpenForms;
int openFormCount = openForms.Count;
for(int index = 0; index < openFormCount; ++index) {
Form form = openForms[index];
IntPtr handle;
if(form.InvokeRequired) {
handle = (IntPtr)form.Invoke(new Func<Form, IntPtr>(getWindowHandle), form);
} else {
handle = getWindowHandle(form);
}
if(handle != IntPtr.Zero) {
if(handle == mainWindowHandle) {
return form;
}
}
}
// No matching main window found: use the first one in good faith or fail.
if(openFormCount > 0) {
return openForms[0];
} else {
return null;
}
}
/// <summary>Checks whether the calling thread needs to use Invoke()</summary>
public bool InvokeRequired {
get { return getMainWindowOrFail().InvokeRequired; }
}
/// <summary>Schedules a method to be run by the main UI thread</summary>
/// <param name="method">Method that will be scheduled to run</param>
/// <param name="args">Arguments that will be passed to the method</param>
/// <returns>An asynchronous result handle that can be used to track the call</returns>
public IAsyncResult BeginInvoke(Delegate method, object[] args) {
return getMainWindowOrFail().BeginInvoke(method, args);
}
/// <summary>Waits for a call scheduled on the main UI thread to complete</summary>
/// <param name="result">Asynchronous result handle returned by BeginInvoke()</param>
/// <returns>The value returned by the method ran in the main UI thread</returns>
public object EndInvoke(IAsyncResult result) {
return getMainWindowOrFail().EndInvoke(result);
}
/// <summary>Executes a method on the main UI thread and waits for it to complete</summary>
/// <param name="method">Method that will be run by the main UI thread</param>
/// <param name="arguments">Arguments that will be passed to the method</param>
/// <returns>The value returned by the method</returns>
public object Invoke(Delegate method, object[] arguments) {
return getMainWindowOrFail().Invoke(method, arguments);
}
/// <summary>Retrieves the application's current main window</summary>
/// <returns>The application's current main window</returns>
/// <remarks>
/// If there is no main window, an exception will be thrown
/// </remarks>
private Form getMainWindowOrFail() {
Form mainWindow = GetMainWindow();
if(mainWindow == null) {
throw new InvalidOperationException(
"Could not schedule work for the UI thread because no WinForms UI main window " +
"was found. Create a main window first or specify the UI synchronization context " +
"explicitly to the view model."
);
}
if(this.uiContextFoundCallback != null) {
this.uiContextFoundCallback(mainWindow);
this.uiContextFoundCallback = null;
}
return mainWindow;
}
/// <summary>Returns a Form's window handle without forcing its creation</summary>
/// <param name="form">Form whose window handle will be returned</param>
/// <returns>The form's window handle of IntPtr.Zero if it has none</returns>
private static IntPtr getWindowHandle(Form form) {
if(form.IsHandleCreated) {
return form.Handle;
} else {
return IntPtr.Zero;
}
}
/// <summary>Called when the late-checked synchronizer finds the main window</summary>
private Action<ISynchronizeInvoke> uiContextFoundCallback;
}
} // namespace Nuclex.Windows.Forms

View File

@ -93,7 +93,10 @@ namespace Nuclex.Windows.Forms.ViewModels {
/// <summary>Initializes a threaded action that uses its own thread runner</summary> /// <summary>Initializes a threaded action that uses its own thread runner</summary>
public ThreadedAction(ISynchronizeInvoke uiContext = null) : this() { public ThreadedAction(ISynchronizeInvoke uiContext = null) : this() {
if(uiContext == null) { if(uiContext == null) {
this.uiContext = getMainWindow(); this.uiContext = LateCheckedSynchronizer.GetMainWindow();
if(this.uiContext == null) {
this.uiContext = new LateCheckedSynchronizer(updateUiContext);
}
} else { } else {
this.uiContext = uiContext; this.uiContext = uiContext;
} }
@ -112,7 +115,10 @@ namespace Nuclex.Windows.Forms.ViewModels {
ThreadedViewModel viewModel, ISynchronizeInvoke uiContext = null ThreadedViewModel viewModel, ISynchronizeInvoke uiContext = null
) : this() { ) : this() {
if(uiContext == null) { if(uiContext == null) {
this.uiContext = getMainWindow(); this.uiContext = LateCheckedSynchronizer.GetMainWindow();
if(this.uiContext == null) {
this.uiContext = new LateCheckedSynchronizer(updateUiContext);
}
} else { } else {
this.uiContext = uiContext; this.uiContext = uiContext;
} }
@ -350,30 +356,10 @@ namespace Nuclex.Windows.Forms.ViewModels {
this.uiContext.Invoke(this.reportErrorDelegate, new object[1] { exception }); this.uiContext.Invoke(this.reportErrorDelegate, new object[1] { exception });
} }
/// <summary>Finds the application's main window</summary> /// <summary>Sets the UI context that will be used by the threaded action</summary>
/// <returns>Main window of the application</returns> /// <param name="uiContext">The UI context the threaded action will use</param>
private static Form getMainWindow() { private void updateUiContext(ISynchronizeInvoke uiContext) {
IntPtr mainWindowHandle = System.Diagnostics.Process.GetCurrentProcess().MainWindowHandle; this.uiContext = uiContext;
// We can get two things: a list of all open windows and the handle of
// the window that the process has registered as main window. Use the latter
// to pick the correct window from the former.
FormCollection openForms = Application.OpenForms;
int openFormCount = openForms.Count;
for(int index = 0; index < openFormCount; ++index) {
if(openForms[index].IsHandleCreated) {
if(openForms[index].Handle == mainWindowHandle) {
return openForms[index];
}
}
}
// No matching main window found: use the first one in good faith or fail.
if(openFormCount > 0) {
return openForms[0];
} else {
return null;
}
} }
/// <summary>Synchronization context of the thread in which the view runs</summary> /// <summary>Synchronization context of the thread in which the view runs</summary>

View File

@ -64,7 +64,10 @@ namespace Nuclex.Windows.Forms.ViewModels {
/// </param> /// </param>
protected ThreadedViewModel(ISynchronizeInvoke uiContext = null) { protected ThreadedViewModel(ISynchronizeInvoke uiContext = null) {
if(uiContext == null) { if(uiContext == null) {
this.uiContext = getMainWindow(); this.uiContext = LateCheckedSynchronizer.GetMainWindow();
if(this.uiContext == null) {
this.uiContext = new LateCheckedSynchronizer(updateUiContext);
}
} else { } else {
this.uiContext = uiContext; this.uiContext = uiContext;
} }
@ -208,30 +211,10 @@ namespace Nuclex.Windows.Forms.ViewModels {
this.uiContext.Invoke(this.reportErrorDelegate, new object[1] { exception }); this.uiContext.Invoke(this.reportErrorDelegate, new object[1] { exception });
} }
/// <summary>Finds the application's main window</summary> /// <summary>Sets the UI context that will be used by the threaded action</summary>
/// <returns>Main window of the application</returns> /// <param name="uiContext">The UI context the threaded action will use</param>
private static Form getMainWindow() { private void updateUiContext(ISynchronizeInvoke uiContext) {
IntPtr mainWindowHandle = System.Diagnostics.Process.GetCurrentProcess().MainWindowHandle; this.uiContext = uiContext;
// We can get two things: a list of all open windows and the handle of
// the window that the process has registered as main window. Use the latter
// to pick the correct window from the former.
FormCollection openForms = Application.OpenForms;
int openFormCount = openForms.Count;
for(int index = 0; index < openFormCount; ++index) {
if(openForms[index].IsHandleCreated) {
if(openForms[index].Handle == mainWindowHandle) {
return openForms[index];
}
}
}
// No matching main window found: use the first one in good faith or fail.
if(openFormCount > 0) {
return openForms[0];
} else {
return null;
}
} }
/// <summary>An array of zero objects</summary> /// <summary>An array of zero objects</summary>

View File

@ -32,6 +32,10 @@ namespace Nuclex.Windows.Forms.Views {
/// </remarks> /// </remarks>
object DataContext { get; set; } object DataContext { get; set; }
// Whether the view owns its view model and it needs to be disposed after
// the view ceases to exist
//bool IsOwnedByView { get; set; }
} }
} // namespace Nuclex.Windows.Forms.Views } // namespace Nuclex.Windows.Forms.Views

View File

@ -72,7 +72,9 @@ namespace Nuclex.Windows.Forms.Views {
get { return this.dataContext; } get { return this.dataContext; }
set { set {
if(value != this.dataContext) { if(value != this.dataContext) {
object oldDataContext = this.dataContext;
this.dataContext = value; this.dataContext = value;
OnDataContextChanged(this, oldDataContext, value);
} }
} }
} }

View File

@ -72,7 +72,9 @@ namespace Nuclex.Windows.Forms.Views {
get { return this.dataContext; } get { return this.dataContext; }
set { set {
if(value != this.dataContext) { if(value != this.dataContext) {
object oldDataContext = this.dataContext;
this.dataContext = value; this.dataContext = value;
OnDataContextChanged(this, oldDataContext, value);
} }
} }
} }

View File

@ -260,6 +260,7 @@ namespace Nuclex.Windows.Forms {
viewType = findBestMatch( viewType = findBestMatch(
namespaceTypes, namespaceTypes,
viewName + "View", viewName + "View",
viewName + "Page",
viewName + "Form", viewName + "Form",
viewName + "Window", viewName + "Window",
viewName + "Dialog", viewName + "Dialog",
@ -272,6 +273,7 @@ namespace Nuclex.Windows.Forms {
viewType = findBestMatch( viewType = findBestMatch(
exportedTypes, exportedTypes,
viewName + "View", viewName + "View",
viewName + "Page",
viewName + "Form", viewName + "Form",
viewName + "Window", viewName + "Window",
viewName + "Dialog", viewName + "Dialog",