using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Reflection; using System.Runtime.InteropServices; 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 { #region struct RedrawLockScope /// Prevents controls from redrawing themselves for a while private struct RedrawLockScope : IDisposable { /// Window message that enables or disables control redraw private const int WM_SETREDRAW = 11; /// Sends a window message to the specified window /// Window a message will be sent to /// ID of the message that will be sent /// First argument to the window procedure /// Second argument to the window procedure /// The return value of the window procedure [DllImport("user32")] public static extern int SendMessage( IntPtr windowHandle, int messageId, bool firstArgument, int secondArgument ); /// Stops redrawing the specified control /// Control to stop redrawing public RedrawLockScope(Control control) { if(Environment.OSVersion.Platform == PlatformID.Win32NT) { SendMessage(control.Handle, WM_SETREDRAW, false, 0); this.control = control; } else { this.control = null; } } /// Enables redrawing again when the lock scope is disposed public void Dispose() { if(this.control != null) { SendMessage(this.control.Handle, WM_SETREDRAW, true, 0); this.control.Invalidate(true); } } /// Control that has been stopped from redrawing itself private Control control; } #endregion // struct RedrawLockScope /// Initializes a new multi page view window for the windows forms designer public MultiPageViewForm() { this.createViewMethod = typeof(IWindowManager).GetMethod(nameof(IWindowManager.CreateView)); } /// 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(nameof(IWindowManager.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); if(arguments.AreAffecting(nameof(MultiPageViewModel.ActivePage))) { var viewModelAsMultiPageviewModel = DataContext as IMultiPageViewModel; if(viewModelAsMultiPageviewModel != null) { if(InvokeRequired) { Invoke( new Action(activatePageView), viewModelAsMultiPageviewModel.GetActivePageViewModel() ); } else { activatePageView(viewModelAsMultiPageviewModel.GetActivePageViewModel()); } } } } /// Currently active page view control protected Control ActivePageView { get { return this.activePageView; } } /// The view model running the currently active page protected object ActivePageViewModel { get { var activePageViewAsView = this.activePageView as IView; if(activePageViewAsView == null) { return null; } else { return activePageViewAsView.DataContext; } } } /// 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 { if(pageViewModel == null) { disableActivePageView(); } else { Control pageViewContainer = getPageViewContainer(); using(new RedrawLockScope(pageViewContainer)) { disableActivePageView(); this.activePageView = getOrCreatePageView(pageViewModel); 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