reactivate exiting window using WindowManager - wpf

I am using WPF with the currently latest and greatest version of Caliburn.Micro (1.4.1). I use IWindowManager.ShowWindow(...) to open an new modeless window:
private void OpenOrReactivateInfoView()
{
if(this.infoViewModel == null)
{
this.infoViewModel = new InfoViewModel();
}
this.windowManager.ShowWindow(this.infoViewModel);
}
Instead of opening a new window each time when OpenOrReactivateInfoView() is called, I would like to check whether the window ist still open and if it is, the existing window should just regain focus.
What would we be a good Calibrun.Micro-way to solve this? I sure would like to avoid keeping a reference to the window (or any UIElement for that matter) itself in the viewmodel. Also note that this is a common behavior for a lot of modeless dialogs, so it is preferred solve this in a generic reusable way.
Does Caliburn.Micro already have means for this built in?

The WindowManager source code always creates a new window, so what you really want to do is only use the WindowManager.ShowWindow method if you actually intend to create a new window.
The first thing you want to do is hold a persistent reference to your view model like this:
private readonly InfoViewModel infoViewModel = new InfoViewModel();
private void OpenOrReactivateInfoView()
{
this.windowManager.ShowWindow(this.infoViewModel);
}
Then, in your view model, create a method called Focus or whatever you want like this:
public void Focus()
{
var window = GetView() as Window;
if (window != null) window.Activate();
}
Then revisit your OpenOrReactivateInfoView() method make a slight adjustment like this:
private void OpenOrReactivateInfoView()
{
if (!this.infoViewModel.IsActive)
this.windowManager.ShowWindow(this.infoViewModel);
else
this.infoViewModel.Focus();
}
This method worked for me.

A fairly straightforward way to keep track of your windows without actually
having to implement IViewAware would be to keep a dictionary of weak references
to your ViewModels and Views that go together and then checking if you already
have an existing Window or not. Could be implemented either as a decorator to
the WindowManager, subclass or extension.
Something as simple as the following should do the trick assuming you don't
actually plan on opening enough windows that even the dead WeakReferences
would impact performance. If it is going to be long running it shouldn't be
that hard to implement some sort of cleanup.
public class MyFancyWindowManager : WindowManager
{
IDictionary<WeakReference, WeakReference> windows = new Dictionary<WeakReference, WeakReference>();
public override void ShowWindow(object rootModel, object context = null, IDictionary<string, object> settings = null)
{
NavigationWindow navWindow = null;
if (Application.Current != null && Application.Current.MainWindow != null)
{
navWindow = Application.Current.MainWindow as NavigationWindow;
}
if (navWindow != null)
{
var window = CreatePage(rootModel, context, settings);
navWindow.Navigate(window);
}
else
{
var window = GetExistingWindow(rootModel);
if (window == null)
{
window = CreateWindow(rootModel, false, context, settings);
windows.Add(new WeakReference(rootModel), new WeakReference(window));
window.Show();
}
else
{
window.Focus();
}
}
}
protected virtual Window GetExistingWindow(object model)
{
if(!windows.Any(d => d.Key.IsAlive && d.Key.Target == model))
return null;
var existingWindow = windows.Single(d => d.Key.Target == model).Value;
return existingWindow.IsAlive ? existingWindow.Target as Window : null;
}
}

I have come up with this extension method. It works but I am not particulary happy with it, it is still somewhat hackish.
It is clearly a designsmell that this extension has to make so many assumption about the model (do you see also those nasty exceptions?).
using System;
using System.Collections.Generic;
using Caliburn.Micro;
public static class WindowManagerExtensions
{
/// <summary>
/// Shows a non-modal window for the specified model or refocuses the exsiting window.
/// </summary>
/// <remarks>
/// If the model is already associated with a view and the view is a window that window will just be refocused
/// and the parameter <paramref name="settings"/> is ignored.
/// </remarks>
public static void FocusOrShowWindow(this IWindowManager windowManager,
object model,
object context = null,
IDictionary<string, object> settings = null)
{
var activate = model as IActivate;
if (activate == null)
{
throw new ArgumentException(
string.Format("An instance of type {0} is required", typeof (IActivate)), "model");
}
var viewAware = model as IViewAware;
if (viewAware == null)
{
throw new ArgumentException(
string.Format("An instance of type {0} is required", typeof (IViewAware)), "model");
}
if (!activate.IsActive)
{
windowManager.ShowWindow(model, context, settings);
return;
}
var view = viewAware.GetView(context);
if (view == null)
{
throw new InvalidOperationException("View aware that is active must have an attached view.");
}
var focus = view.GetType().GetMethod("Focus");
if (focus == null)
{
throw new InvalidOperationException("Attached view requires to have a Focus method");
}
focus.Invoke(view, null);
}
}

Related

How to destroy or detach a CollectionView

I observe an odd behaviour of WPF ItemsControls: If a set the ItemsSource to an object which implements INotifyCollectionChanged and after that set the ItemsSource to null, the CollectionView which was created to provide the data to the ItemsControl still listens to the CollectionChanged-event of the source object.
If now the source collection is changed through a different thread, the CollectionView throws an exception (without being attached to any control).
While I understand why this is happening, I’m really stuck resolving this situation.
Therefore the main question is, how can I destroy a CollectionView so that it does not listen any more to CollectionChanged-event. Or how can I disable it doing that / detaching the underlying collection.
Please note: The described behavior is not with ObservableCollection. The source object is an IEnumerable of T and implements INotifyCollectionChanged.
You're looking for the CollectionView.DetachFromSourceCollection() method:
var collectionView = CollectionViewSource.GetDefaultView(yourEnumerable) as CollectionView;
collectionView.DetachFromSourceCollection();
Update
It seems, that under .net 4.5 there is this desired functionality. See the answer of HighCore. For those not having 4.5 I leave my workaround here, maybe it helps someone:
class DetachableNotifyCollectionChangedWrapper : IEnumerable, INotifyCollectionChanged {
IEnumerable m_source;
public event NotifyCollectionChangedEventHandler CollectionChanged;
public DetachableNotifyCollectionChangedWrapper(IEnumerable enumerable) {
if (null == enumerable) throw new ArgumentNullException("enumerable"); ;
m_source = enumerable;
var ncc = m_source as INotifyCollectionChanged;
if (null != ncc) ncc.CollectionChanged += SourceCollectionChanged;
}
void SourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) {
if (null != CollectionChanged) CollectionChanged(this,e);
}
public IEnumerator GetEnumerator() {
return m_source.GetEnumerator();
}
public void Detach() {
var ncc = m_source as INotifyCollectionChanged;
if (null != ncc) ncc.CollectionChanged -= SourceCollectionChanged;
}
}
To use this, set the Wrapper as the ItemsSource of the ItemsControl. Before setting then the ItemsSource to null, call Detach on the wrapper to unregister the changed-event. Something as follows:
var wrapper = m_lstLog.ItemsSource as DetachableNotifyCollectionChangedWrapper;
if (null != wrapper) wrapper.Detach();
m_lstLog.ItemsSource = null;
The wrapper can also be used from within a ViewModel.

Setting ToolStripDropDown.DropShadowEnabled to false on multi level dropdowns

I want to disable the dropdown shadow on the dropdown of a ToolStripDropDownButton. If the dropdown menu contains items that have dropdowns themselves (e.g. multi-level menu) then setting the DropShadowEnabled to false on the ToolStripDropDownButton causes the top level dropdown to appear at the wrong position. See attached picture.
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
toolStripDropDownButton1.DropDown.DropShadowEnabled = false;
}
}
partial class Form1
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Form1));
this.toolStrip1 = new System.Windows.Forms.ToolStrip();
this.toolStripDropDownButton1 = new System.Windows.Forms.ToolStripDropDownButton();
this.item1ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.subitem1ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.toolStrip1.SuspendLayout();
this.SuspendLayout();
//
// toolStrip1
//
this.toolStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.toolStripDropDownButton1});
this.toolStrip1.Location = new System.Drawing.Point(0, 0);
this.toolStrip1.Name = "toolStrip1";
this.toolStrip1.Size = new System.Drawing.Size(292, 25);
this.toolStrip1.TabIndex = 0;
this.toolStrip1.Text = "toolStrip1";
//
// toolStripDropDownButton1
//
this.toolStripDropDownButton1.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image;
this.toolStripDropDownButton1.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.item1ToolStripMenuItem});
this.toolStripDropDownButton1.Image = ((System.Drawing.Image)(resources.GetObject("toolStripDropDownButton1.Image")));
this.toolStripDropDownButton1.ImageTransparentColor = System.Drawing.Color.Magenta;
this.toolStripDropDownButton1.Name = "toolStripDropDownButton1";
this.toolStripDropDownButton1.Size = new System.Drawing.Size(29, 22);
this.toolStripDropDownButton1.Text = "toolStripDropDownButton1";
//
// item1ToolStripMenuItem
//
this.item1ToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.subitem1ToolStripMenuItem});
this.item1ToolStripMenuItem.Name = "item1ToolStripMenuItem";
this.item1ToolStripMenuItem.Size = new System.Drawing.Size(152, 22);
this.item1ToolStripMenuItem.Text = "item1";
//
// subitem1ToolStripMenuItem
//
this.subitem1ToolStripMenuItem.Name = "subitem1ToolStripMenuItem";
this.subitem1ToolStripMenuItem.Size = new System.Drawing.Size(152, 22);
this.subitem1ToolStripMenuItem.Text = "subitem1";
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(292, 273);
this.Controls.Add(this.toolStrip1);
this.Name = "Form1";
this.Text = "Form1";
this.toolStrip1.ResumeLayout(false);
this.toolStrip1.PerformLayout();
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.ToolStrip toolStrip1;
private System.Windows.Forms.ToolStripDropDownButton toolStripDropDownButton1;
private System.Windows.Forms.ToolStripMenuItem item1ToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem subitem1ToolStripMenuItem;
}
This is very typical lossage in the ToolStripItem classes. Clearly it is a bug, it probably got introduced when they applied a hack to work around a Windows problem. You can still see the internal bug number in the Reference Source:
public bool DropShadowEnabled {
get {
// VSWhidbey 338272 - DropShadows are only supported on TopMost windows
// due to the flakeyness of the way it's implemented in the OS. (Non toplevel
// windows can have parts of the shadow disappear because another window can get
// sandwiched between the SysShadow window and the dropdown.)
return dropShadowEnabled && TopMost && DisplayInformation.IsDropShadowEnabled;
}
set { // etc... }
}
But without a corresponding fix in the setter and the renderer.
The flakeyness they mentioned actually got fixed in Vista, you are still running on XP so you can't see it. Drop shadows are done differently on the Aero desktop and it is a system setting whether or not they are enabled. So using the property is entirely ineffective on Aero.
These ToolStripItem class bugs didn't get fixed after the .NET 2.0 release, about the entire Winforms team moved over to the WPF group. And they are certainly not getting fixed now, no point filing a bug at connect.microsoft.com although you are free to do so.
With the added wrinkle that the property just cannot have an effect anymore on later versions of Windows since it is now a system setting, the only logical thing to do here is to throw in the towel. Don't change the property.

Databinding setting to null and starting new

I have in my App class an object Order, both implementing INotfiyPropertyChanged.
When an Order is concluded, I set it to null and do a new Order() for restarting a new Order.
The problem is: it seems the objects that whose DataContext was bound to Order seems they are always linked to the older Order
What can I do to for not having to rebind again manually when I restart an Order?
For every object with this DataContext, I need to do object.DataContext= App.Order. What can I do to avoid this?
Some code:
public partial class App : Application, INotifyPropertyChanged
{
private Order m_order = new Order();
public Order Order
{
get { return m_order; }
set
{
m_order = value;
NotifyPropertyChanged("Order");
}
}
//...
public bool getOrderClosed()
{
if (Order != null)
{
Order = null;
}
return (Order == null);
}
public bool getOrderOpened()
{
if (Order == null)
Order = new Order();
return (Order != null);
}
private void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
//code on the part where the order is finished
private void Confirm_Click(object sender, RoutedEventArgs e)
{
//...
if (SaveOrder())
{
theApp.getOrderClosed();
theApp.getOrderOpened();
theApp.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.Basket.DataContext = theApp.Order;
}
}
If I understand correctly, your Order object is the DataContext. If you're setting the DataContext in codebehind, it'd looks something like:
[some_element].DataContext = myApp.Order;
What you'd need to do is bind the DataContext to myApp.Order. This way, when you do something like
myApp.Order = new Order(...);
the DataContext for [some_element] will change as well. If you post your XAML code where you're using the Order object as the DataContext, I can show you exactly what your binding on the DataContext should look like.
With what you've done, only the changes within the DataContext will be picked up; when you change the property that you're using as the DataContext changes, the DataContext itself does not.
I had a similar problem a few weeks ago. I found that when assigning a bound property to a new object the binding was lost, so as a workaround I had to create a new temp object and then copy all the fields into my bound property, that way you effectivly have a fresh object and the binding is maintained.
Hope this helps!
Maybe change the binding to UpdateSourceTrigger=PropertyChanged?
If App implements INotifyPropertyChanged properly, it will fire when you set App.Order to a new instance, and the binding will update.
Are you sure it is implemented properly?
eg.
class App
{
public Order Order
{
get
{
return _order;
}
set
{
if (value != _order)
{
_order = value;
FirePropertyChanged("Order");
}
}
}
}

How to create input gesture for page/window from an user control

I have a reusable usercontrol that uses a few commands and corresponding keyboard gestures,
(specifically Escape and Ctrl+1...Ctrl+9)
Now as I use this usercontrol in multiple locations I'd like to define the input gestures in the usercontrol, which works fine as long as the focus is within the usercontrol. However, I'd need it to work as long as focus is within the current page/window.
How can I do it, or do I really have to do command/input bindings on every page?
You could handle the Loaded event of the UserControl and walk up the logical tree to find the owning page/window, then you can add the bindings there.
e.g.
public partial class Bogus : UserControl
{
public Bogus()
{
Loaded += (s, e) => { HookIntoWindow(); };
InitializeComponent();
}
private void HookIntoWindow()
{
var current = this.Parent;
while (!(current is Window) && current is FrameworkElement)
{
current = ((FrameworkElement)current).Parent;
}
if (current != null)
{
var window = current as Window;
// Add input bindings
var command = new AlertCommand();
window.InputBindings.Add(new InputBinding(command, new KeyGesture(Key.D1, ModifierKeys.Control)));
}
}
}

WPF doesn't honor Textbox.MinLines for Auto height calculation

I want to have a TextBox which Height grows as Iam entering lines of Text.
I've set the Height property to "Auto", and so far the growing works.
Now I want that the TextBox's Height should be at least 5 lines.
Now I've set the MinLines property to "5" but if I start the app the TextBox's height is still one line.
Try setting the MinHeight property.
A hack to make the MinLines property work
public class TextBoxAdv : TextBox
{
bool loaded = false;
/// <summary>
/// Constructor
/// </summary>
public TextBoxAdv()
{
Loaded += new RoutedEventHandler( Control_Loaded );
SetResourceReference( StyleProperty, typeof( TextBox ) );
}
void Control_Loaded( object sender, RoutedEventArgs e )
{
if( !loaded )
{
loaded = true;
string text = Text;
Text = "Text";
UpdateLayout();
Text = text;
}
}
}
I propose a different solution that properly respects the MinLines property, rather than forcing you to use MinHeight.
First, start with a convenience method to allow you to Post an action to the window loop. (I'm including both one where you need to pass state and one where you don't.)
public static class Globals {
public static void Post(Action callback){
if(SynchronizationContext.Current is SynchronizationContext currentContext)
currentContext.Post( _ => callback(), null);
else{
callback();
}
}
public static void Post<TState>(TState state, Action<TState> callback){
if(SynchronizationContext.Current is SynchronizationContext currentContext)
currentContext.Post(_ => callback(state), null);
else{
callback(state);
}
}
}
Next, create an extension method for TextBox to 'initialize' the proper size based on MinLines. I put this in a Hacks class because to me, that's what this is and it clearly identifies the code as such.
public static void FixInitialMinLines(this TextBox textBox) {
Globals.Post(() => {
var textBinding = textBox.GetBindingExpression(TextBox.TextProperty)?.ParentBinding;
if (textBinding != null) {
BindingOperations.ClearBinding(textBox, TextBox.TextProperty);
textBox.UpdateLayout();
BindingOperations.SetBinding(textBox, TextBox.TextProperty, textBinding);
}
else {
var lastValue = textBox.Text;
textBox.Text = lastValue + "a";
textBox.UpdateLayout();
textBox.Text = lastValue;
}
});
}
The above code handles both bound and unbound TextBox controls, but rather than simply changing the value like other controls which may cascade that change down through the bindings, it first disconnects the binding, forces layout, then reconnects the binding, thus triggering the proper layout in the UI. This avoids unintentionally changing your bound sources should the binding be two-way.
Finally, simply call the extension method for every TextBox where MinLines is set. Thanks to the Post call in the extension method, You can call this immediately after InitializeComponent and it will still be executed after all other events have fired, including all layout and the Loaded event.
public partial class Main : Window {
public Main() {
InitializeComponent();
// Fix initial MinLines issue
SomeTextBoxWithMinLines.FixInitialMinLines();
}
...
}
Add the above code to your 'library' of functions and you can address the issue with a single line of code in all of your windows and controls. Enjoy!

Resources