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