diff --git a/Nuclex.Windows.Forms (net-4.0).csproj b/Nuclex.Windows.Forms (net-4.0).csproj
index 6552027..5ce0dec 100644
--- a/Nuclex.Windows.Forms (net-4.0).csproj
+++ b/Nuclex.Windows.Forms (net-4.0).csproj
@@ -82,6 +82,7 @@
DialogViewModel.cs
+
@@ -98,6 +99,9 @@
+
+ Form
+
UserControl
diff --git a/Source/Controls/ProgressSpinner.cs b/Source/Controls/ProgressSpinner.cs
index ee423bc..967bc58 100644
--- a/Source/Controls/ProgressSpinner.cs
+++ b/Source/Controls/ProgressSpinner.cs
@@ -94,6 +94,29 @@ namespace Nuclex.Windows.Forms.Controls {
}
}
+ /// Calculates the optimal size for the spinner control
+ /// The optimal size for the spinner control to have
+ ///
+ /// Thanks to WinForms limited control transparency, the progress spinner needs to
+ /// redraw every control behind it each time it updates. Thus it's wise to keep it
+ /// as small as possible, but wide enough to fit the status text, if any.
+ ///
+ public Size GetOptimalSize() {
+ SizeF textRectangle;
+ using(var dummyImage = new Bitmap(1, 1)) {
+ using(Graphics graphics = Graphics.FromImage(dummyImage)) {
+ textRectangle = graphics.MeasureString(
+ this.statusText, this.statusFont
+ );
+ }
+ }
+
+ return new Size(
+ Math.Max(128, (int)(textRectangle.Width + 2.0f)),
+ this.statusFont.Height + 128
+ );
+ }
+
/// Font that is used to display the status text
public Font StatusFont {
get { return this.statusFont; }
diff --git a/Source/ViewModels/IMultiPageViewModel.cs b/Source/ViewModels/IMultiPageViewModel.cs
new file mode 100644
index 0000000..39007a1
--- /dev/null
+++ b/Source/ViewModels/IMultiPageViewModel.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace Nuclex.Windows.Forms.ViewModels {
+
+ /// Interface for vew models that can switch between different pages
+ public interface IMultiPageViewModel {
+
+ /// Retrieves (and, if needed, creates) the view model for the active page
+ /// A view model for the active page on the multi-page view model
+ object GetActivePageViewModel();
+
+ }
+
+} // namespace Nuclex.Windows.Forms.ViewModels
diff --git a/Source/ViewModels/MultiPageViewModel.cs b/Source/ViewModels/MultiPageViewModel.cs
index 55af961..355a7cb 100644
--- a/Source/ViewModels/MultiPageViewModel.cs
+++ b/Source/ViewModels/MultiPageViewModel.cs
@@ -19,6 +19,7 @@ License along with this library
#endregion
using System;
+using System.Collections.Concurrent;
using Nuclex.Support;
@@ -26,8 +27,8 @@ namespace Nuclex.Windows.Forms.ViewModels {
/// Base class for view models that have multiple child view models
/// Enum type by which pages can be indicated
- public abstract class MultiPageViewModel : Observable
- where TPageEnumeration : IEquatable {
+ public abstract class MultiPageViewModel :
+ Observable, IMultiPageViewModel, IDisposable {
/// Initializes a new multi-page view model
///
@@ -38,6 +39,25 @@ namespace Nuclex.Windows.Forms.ViewModels {
///
public MultiPageViewModel(IWindowManager windowManager, bool cachePageViewModels = false) {
this.windowManager = windowManager;
+ if(cachePageViewModels) {
+ this.cachedViewModels = new ConcurrentDictionary();
+ }
+ }
+
+ /// Immediately releases all resources owned by the instance
+ public virtual void Dispose() {
+ if(this.cachedViewModels != null) {
+ foreach(object cacheViewModel in this.cachedViewModels.Values) {
+ disposeIfSupported(cacheViewModel);
+ }
+ this.activePageViewModel = null;
+
+ this.cachedViewModels.Clear();
+ this.cachedViewModels = null;
+ } else if(this.activePageViewModel != null) {
+ disposeIfSupported(this.activePageViewModel);
+ this.activePageViewModel = null;
+ }
}
/// Child page that is currently being displayed by the view model
@@ -46,11 +66,36 @@ namespace Nuclex.Windows.Forms.ViewModels {
set {
if(!this.activePage.Equals(value)) {
this.activePage = value;
+ if(this.activePageViewModel != null) {
+ if(this.cachedViewModels == null) {
+ disposeIfSupported(this.activePageViewModel);
+ }
+ this.activePageViewModel = null;
+ }
OnPropertyChanged(nameof(ActivePage));
}
}
}
+ /// Retrieves (and, if needed, creates) the view model for the active page
+ /// A view model for the active page on the multi-page view model
+ public object GetActivePageViewModel() {
+ if(this.cachedViewModels == null) {
+ if(this.activePageViewModel == null) {
+ this.activePageViewModel = CreateViewModelForPage(this.activePage);
+ }
+ } else if(this.activePageViewModel == null) {
+ this.activePageViewModel = this.cachedViewModels.GetOrAdd(
+ this.activePage,
+ delegate(TPageEnumeration activePage) {
+ return CreateViewModelForPage(this.activePage);
+ }
+ );
+ }
+
+ return this.activePageViewModel;
+ }
+
/// Windowmanager that can create view models and display other views
protected IWindowManager WindowManager {
get { return this.windowManager; }
@@ -59,13 +104,27 @@ namespace Nuclex.Windows.Forms.ViewModels {
/// Creates a view model for the specified page
/// Page for which a view model will be created
/// The view model for the specified page
- protected abstract object createViewModelForPage(TPageEnumeration page);
+ protected abstract object CreateViewModelForPage(TPageEnumeration page);
+
+ /// Disposes the specified object if it is disposable
+ /// Object that will be disposed if supported
+ private static void disposeIfSupported(object potentiallyDisposable) {
+ var disposable = potentiallyDisposable as IDisposable;
+ if(disposable != null) {
+ disposable.Dispose();
+ }
+ }
/// Page that is currently active in the multi-page view model
private TPageEnumeration activePage;
/// Window manager that can be used to display other views
private IWindowManager windowManager;
+ /// View model for the active page
+ private object activePageViewModel;
+ /// Cached page view models, if caching is enabled
+ private ConcurrentDictionary cachedViewModels;
+
}
} // namespace Nuclex.Windows.Forms.ViewModels
diff --git a/Source/Views/MultiPageViewForm.cs b/Source/Views/MultiPageViewForm.cs
new file mode 100644
index 0000000..f0a1a6c
--- /dev/null
+++ b/Source/Views/MultiPageViewForm.cs
@@ -0,0 +1,287 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Drawing;
+using System.Reflection;
+using System.Windows.Forms;
+
+using Nuclex.Support;
+using Nuclex.Windows.Forms.ViewModels;
+
+namespace Nuclex.Windows.Forms.Views {
+
+ /// Special view form that can display different child views
+ public class MultiPageViewForm : ViewForm {
+
+ /// Initializes a new multi page view window
+ ///
+ /// Window manager that is used to set up the child views
+ ///
+ /// Whether page views should be kept alive and reused
+ public MultiPageViewForm(IWindowManager windowManager, bool cachePageViews = false) {
+ this.windowManager = windowManager;
+ this.createViewMethod = typeof(IWindowManager).GetMethod("CreateView");
+
+ if(cachePageViews) {
+ this.cachedViews = new Dictionary();
+ }
+ }
+
+ /// Called when the control is being disposed
+ ///
+ /// Whether the call was made by user code (vs. the garbage collector)
+ ///
+ protected override void Dispose(bool calledExplicitly) {
+ if(calledExplicitly) {
+
+ // Disable the active view, if any
+ if(this.activePageView != null) {
+ if(this.childViewContainer != null) {
+ this.childViewContainer.Controls.Remove(this.activePageView);
+ }
+ }
+
+ // If caching is disabled, dispose of the active child view, if any
+ if(this.cachedViews == null) {
+ if(this.activePageView != null) {
+ disposeIfSupported(this.activePageView);
+ this.activePageView = null;
+ }
+ } else { // Caching is enabled, dispose of any cached child views
+ foreach(Control childView in this.cachedViews.Values) {
+ disposeIfSupported(childView);
+ }
+ this.cachedViews.Clear();
+ this.cachedViews = null;
+ this.activePageView = null;
+ }
+ }
+
+ base.Dispose(calledExplicitly);
+ }
+
+ /// Discovers the container control used to host the child views
+ /// The container control is which the child views will be hosted
+ ///
+ /// This is supposed to be overriden by the user, simply returning the container
+ /// control that should host the page views. If it isn't, however, we use some
+ /// heuristics to figure out the most likely candidate: it should be a container,
+ /// and it should cover most of the window's client area.
+ ///
+ protected virtual Control IdentifyPageContainer() {
+ Size halfWindowSize = Size;
+ halfWindowSize.Width /= 2;
+ halfWindowSize.Height /= 2;
+
+ // First container control we found -- if we find no likely candidate,
+ // we simply use the first
+ Control firstContainer = null;
+
+ // Check all top-level controls in the window. If there's a container that
+ // covers most of the window, it's our best bet
+ int controlCount = Controls.Count;
+ for(int index = 0; index < controlCount; ++index) {
+ Control control = Controls[index];
+
+ // Only check container controls
+ if((control is ContainerControl) || (control is Panel)) {
+ if(firstContainer == null) {
+ firstContainer = control;
+ }
+
+ // If this control covers most of the view, it's our candidate!
+ Size controlSize = control.Size;
+ bool goodCandidate = (
+ (controlSize.Width > halfWindowSize.Width) &&
+ (controlSize.Height > halfWindowSize.Height)
+ );
+ if(goodCandidate) {
+ return control;
+ }
+ }
+ }
+
+ // If no candidate was found, return the first container control we encountered
+ // or create a new UserControl as the container if nothing was found at all.
+ if(firstContainer == null) {
+ firstContainer = new Panel();
+ Controls.Add(firstContainer);
+ firstContainer.Dock = DockStyle.Fill;
+ }
+
+ return firstContainer;
+ }
+
+ /// Called when the window's data context is changed
+ /// Window whose data context was changed
+ /// Data context that was previously used
+ /// Data context that will be used from now on
+ protected override void OnDataContextChanged(
+ object sender, object oldDataContext, object newDataContext
+ ) {
+
+ // Kill the currently active view if there was an old view model.
+ if(oldDataContext != null) {
+ disableActivePageView();
+ }
+
+ base.OnDataContextChanged(sender, oldDataContext, newDataContext);
+
+ // If a valid view model was assigned, create a new view its active page view model
+ if(newDataContext != null) {
+ var dataContextAsMultiPageViewModel = newDataContext as IMultiPageViewModel;
+ if(dataContextAsMultiPageViewModel != null) {
+ activatePageView(dataContextAsMultiPageViewModel.GetActivePageViewModel());
+ }
+ }
+
+ }
+
+ /// Called when a property of the view model is changed
+ /// View model in which a property was changed
+ /// Contains the name of the property that has changed
+ protected override void OnViewModelPropertyChanged(
+ object sender, PropertyChangedEventArgs arguments
+ ) {
+ base.OnViewModelPropertyChanged(sender, arguments);
+
+ MultiPageViewModel anyMultiPageViewModel;
+ if(arguments.AreAffecting(nameof(anyMultiPageViewModel.ActivePage))) {
+ var viewModelAsMultiPageviewModel = DataContext as IMultiPageViewModel;
+ if(viewModelAsMultiPageviewModel != null) {
+ activatePageView(viewModelAsMultiPageviewModel.GetActivePageViewModel());
+ }
+ }
+ }
+
+ /// Activates the page view for the specified page view model
+ ///
+ /// Page view model for which the page view will be activated
+ ///
+ private void activatePageView(object pageViewModel) {
+ object activePageViewModel = null;
+ {
+ var activePageViewAsView = this.activePageView as IView;
+ if(activePageViewAsView != null) {
+ activePageViewModel = activePageViewAsView.DataContext;
+ }
+ }
+
+ // Try from the cheapest to the most expensive way to get to our goal,
+ // an activated view suiting the specified view model.
+
+ // If we already have the target view model selected, do nothing
+ if(activePageViewModel == pageViewModel) {
+ return;
+ }
+
+ // If the page view model for the old and the new page are of the same
+ // type, we can reuse the currently active page view
+ if((activePageViewModel != null) && (pageViewModel != null)) {
+ if(pageViewModel.GetType() == this.activePageView.GetType()) {
+ var activePageViewAsView = this.activePageView as IView;
+ if(activePageViewAsView != null) {
+ activePageViewAsView.DataContext = pageViewModel;
+ }
+
+ return;
+ }
+ }
+
+ // Worst, but usual, case: the new page view model might require
+ // a different view. Create or look up the new view and put it in the container
+ {
+ disableActivePageView();
+
+ this.activePageView = getOrCreatePageView(pageViewModel);
+
+ Control pageViewContainer = getPageViewContainer();
+ pageViewContainer.Controls.Add(this.activePageView);
+ this.activePageView.Dock = DockStyle.Fill;
+ }
+ }
+
+ /// Gets the cached child view or creates a new one if not cached
+ /// View model for which a child view will be returned
+ /// A child view suitable for the specified view model
+ private Control getOrCreatePageView(object viewModel) {
+ Type viewModelType = viewModel.GetType();
+
+ Control view;
+
+ // If caching is enabled, check if we have a cached view
+ if(this.cachedViews != null) {
+ if(this.cachedViews.TryGetValue(viewModelType, out view)) {
+ return view;
+ }
+ }
+
+ // Otherwise, call the window manager's CreateView() method
+ MethodInfo specializedCreateViewMethod = (
+ this.createViewMethod.MakeGenericMethod(viewModelType)
+ );
+ view = (Control)specializedCreateViewMethod.Invoke(
+ this.windowManager, new object[1] { viewModel }
+ );
+
+ // If caching is enabled, register the view in the cache
+ if(this.cachedViews != null) {
+ this.cachedViews.Add(viewModelType, view);
+ }
+
+ return view;
+ }
+
+ /// Disables the currently active page view control
+ private void disableActivePageView() {
+ if(this.activePageView != null) {
+ Control container = getPageViewContainer();
+ container.Controls.Remove(this.activePageView);
+
+ // If we don't reuse views, kill it now
+ if(this.cachedViews == null) {
+ disposeIfSupported(this.activePageView);
+ this.activePageView = null;
+ } else {
+ var activePageViewAsView = this.activePageView as IView;
+ if(activePageViewAsView != null) {
+ activePageViewAsView.DataContext = null;
+ }
+ }
+ }
+ }
+
+ /// Fetches the container that holds the child views
+ /// The container for the child views
+ private Control getPageViewContainer() {
+ if(this.childViewContainer == null) {
+ this.childViewContainer = IdentifyPageContainer();
+ }
+
+ return this.childViewContainer;
+ }
+
+ /// Disposes the specified object if it is disposable
+ /// Object that will be disposed if supported
+ private static void disposeIfSupported(object potentiallyDisposable) {
+ var disposable = potentiallyDisposable as IDisposable;
+ if(disposable != null) {
+ disposable.Dispose();
+ }
+ }
+
+ /// Window manager through which the child views are created
+ private IWindowManager windowManager;
+ /// Reflection info for the createView() method of the window manager
+ private MethodInfo createViewMethod;
+
+ /// Container in which the child views will be hosted
+ private Control childViewContainer;
+ /// Cached views that will be reused when the view model activates them
+ private Dictionary cachedViews;
+ /// The currently active child view
+ private Control activePageView;
+
+ }
+
+} // namespace Nuclex.Windows.Forms.Views