Preamble
There are dozens of simular Questions to this topic on StackOverflow already and I browsed a lot without finding a suitable answer, that would apply to my problem.
Task
I have a WPF Window in a MVVM pattern, which has quite a lot of buttons that open other windows. I'd like to have most of my windows appear in relation to my buttons (I have a toolbar in the top right corner of my MainWindow and want most of the smaller windows to appear right below my buttons), or at least on the same screen as my MainWindow.
Problem
At first, I thought this wasn't such a big deal and there were plenty of blogs and questions on google to this topic, yet all of them won't work on my project.
I am using the MVVM pattern, which means:
I can't use Mouse.GetPosition(ButtonName) on my Buttons, as the ViewModel doesn't know their names
I can't use Mouse.GetPosition(sender) in a Click-Event, as most Buttons use commands.
I also apparently can't use PointToScreen in my view's code behind, as it will cause an exception (this visual object is not connected to a \"PresentationSource\")
I could use Mouse.GetPosition(this) on a MouseMove-Event in my view's code behind and hand it down to my ViewModel, which will update a Property, that I can use in my Commands when creating the window, but I don't like the idea of having to update a property permanently. Also without PointToScreen I can't set the point in relation to my screen.
I can't use any WinForms references, as this would cause conflicts in my current project
Additional to Buttons, I also host a UserControl with Hyperlinks in my MainWindow, which open additional windows, that should be in relation to the hyperlinks.
Research
there are quite a few different answers to a question here, but none of them did work for me.
As my ViewModel doesn't know the XAML elements I can't simply access by point notation as suggested here
My ViewModel doesn't know a WorkingArea, so I couldn't even get my window to appear on the same screen as my MainWindow as demonstrated here
As most of the other answers, this one seems like it won't work in a ViewModel
Question
I've spent quite some time on a problem, that rather seemed trivial at first, already. Since most questions I've viewed so far seem to target windows without MVVM, what would be the proper approach in a ViewModel to set the location of a window to either my mouse coordinates or the coordinates of a clicked button?
edit:
MouseDownEvent as requested in Comments:
Xaml:
<Window x:Class="MySampleProject.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:MySampleProject"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525"
MouseDown="Window_MouseDown">
C#:
private void Window_MouseDown(object sender, MouseEventArgs e)
{
if (m_oDataContext != null)
{
m_oDataContext.MouseTest(Mouse.GetPosition(this));
}
}
oDataContext is my ViewModel. My MouseTest() is currently empty. I did set a breakpoint at the first bracket. The breakpoint is only reached when left-clicking within my window, not within one of its hosted controls.
Here comes an example of how you can pass a parameter to Command in your Vm:
Window Class:
public partial class MainWindow
{
public MainWindow()
{
InitializeComponent();
DataContext= new MyVm();
}
private void BtnWin1_OnClick(object sender, RoutedEventArgs e)
{
var dataContext = DataContext as MyVm;
var relativePoint = ((Button)sender).TransformToAncestor(this).Transform(new Point(0, 0));
relativePoint.X += this.Left;
relativePoint.Y += this.Top;
dataContext?.OpenWindow1Command.Execute(relativePoint);
}
private void BtnWin2_OnClick(object sender, RoutedEventArgs e)
{
var dataContext = DataContext as MyVm;
var relativePoint = ((Button)sender).TransformToAncestor(this).Transform(new Point(0, 0));
relativePoint.X += this.Left;
relativePoint.Y += this.Top;
dataContext?.OpenWindow2Command.Execute(relativePoint);
}
}
VM Class:
public class MyVm : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public ICommand OpenWindow1Command { get; }
public ICommand OpenWindow2Command { get; }
public MyVm()
{
OpenWindow1Command = new RelayCommand(OpenWindow1Command_Execute);
OpenWindow2Command = new RelayCommand(OpenWindow2Command_Execute);
}
void OpenWindow1Command_Execute(object parameter)
{
var point = (Point)parameter;
var win1 = new Window1{WindowStartupLocation = WindowStartupLocation.Manual, Left = point.X, Top = point.Y};
win1.Show();
}
void OpenWindow2Command_Execute(object parameter)
{
var point = (Point)parameter;
var win2 = new Window2 { WindowStartupLocation = WindowStartupLocation.Manual, Left = point.X, Top = point.Y };
win2.Show();
}
}
And Relay class if you haven't implemented that:
public class RelayCommand : ICommand
{
private readonly Action<object> _execute;
private readonly Func<bool> _canExecute;
public RelayCommand(Action<object> execute, Func<bool> canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return _canExecute == null || _canExecute.Invoke();
}
public event EventHandler CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
public void Execute(object parameter)
{
_execute(parameter);
}
}
You will loose the CanExecute functionality of the Command with this approach, but will do the work.
Related
I am trying to redirect the Window Closed event to my ViewModel, but lack the proper hands on experience with AttachedProperties.
The class that holds the AttachedProperty
public class WindowClosedBehavior
{
public static readonly DependencyProperty ClosedProperty = DependencyProperty.RegisterAttached(
"Closed",
typeof (ICommand),
typeof (WindowClosedBehavior),
new UIPropertyMetadata(ClosedChanged));
private static void ClosedChanged(
DependencyObject target,
DependencyPropertyChangedEventArgs e)
{
var window = target as Window;
if (window != null)
{
// ??
}
}
public static void SetClosed(Window target, ICommand value)
{
target.SetValue(ClosedProperty, value);
}
}
How can I implement the behavior so that it will close the window and trigger the RelayCommand?
The (stripped) ViewModel :
public RelayCommand WindowClosedCommand { get; private set; }
public MainCommandsViewModel()
{
WindowClosedCommand = new RelayCommand(WindowClosedCommandOnExecuted, WindowClosedCommandOnCanExecute);
}
MainWindow.xaml
<Window x:Class="TvShowManager.UserInterface.Views.MainWindow"
<!-- left out irrelevant parts -->
xmlns:closeBehaviors="clr-namespace:TvShowManager.UserInterface.CloseBehaviors"
closeBehaviors:WindowClosedBehavior.Closed="{Binding WindowCloseCommand}" >
I simply bind a RelayCommand (WindowCloseCommand) to the attached property.
I tried debugging through this to get better understanding and hopefully figure out how to proceed, but no breakpoints are being hit in the class that holds my attached property. If anybody can explain why my code in WindowClosedBehavior never gets executed I would also greatly appreciate the advice there.
I hope it's clear what I am trying to achieve and that somebody can help me out.
Many thanks
Within the ClosedChanged callback, just store the command and register an event handler to the window's Closed event to invoke the command:
private static ICommand _command;
private static void ClosedChanged(
DependencyObject target,
DependencyPropertyChangedEventArgs e)
{
var window = target as Window;
if (window != null)
{
_command = e.NewValue as ICommand;
window.Closed += (sender, args) =>
{
if (_command != null)
_command.Execute(null);
}
}
}
In addition, you might want to un-register all previously existing event handlers on the window's Closed event, but that is only necessary if you plan to change the WindowClosedBenahior during runtime.
I am just starting with WPF and I am trying to setup binding between a local variable and a label. Basicaly I want to update the label when local variable changes. I was searching for solution but they all just use textbox as a source not just class variable and I am not even sure it works this way. So here is my code.
public partial class MainWindow : Window
{
int idCounter;
public MainWindow()
{
InitializeComponent();
Binding b = new Binding();
b.Source = idCounter;
b.Mode = BindingMode.OneWay;
b.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
myLabel.SetBinding(Label.ContentProperty,b);
}
private void Button_Click(object sender, RoutedEventArgs e)
{
idCounter++;
}
}
Button does work, idCounter changes value, but it does not update in label so I guess binding is wrong. Can someone tell me what is wrong? Thanks
Your code will work if you change your class to this...
public partial class Window1 : Window, INotifyPropertyChanged
{
private int _idCounter;
public int IdCounter
{
get { return _idCounter; }
set
{
if (value != _idCounter)
{
_idCounter = value;
OnPropertyChanged("IdCounter");
}
}
}
public Window1()
{
InitializeComponent();
myLabel.SetBinding(ContentProperty, new Binding("IdCounter"));
DataContext = this;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
e.Handled = true;
IdCounter++;
}
#region INotifyPropertyChanged Implementation
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string name)
{
var handler = System.Threading.Interlocked.CompareExchange(ref PropertyChanged, null, null);
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
#endregion
}
Some of the issues you were having are...
The window itself should implement INotifyPropertyChanged so that the binding engine can place an ear on it.
the IdCounter needs to be public and have a public getter on it so that the binding engine can 'get' it.
You should set the DataContext to whatever class has declared IdCounter (the MainWindow in this case). Part of the problem was that the binding engine had no DataContext.
The BindingMode setting was a red-herring since a Label binds that way by default.
The UpdateSourceTrigger was a red-herring since the content of the label does not have a mechanism to update the source property. A label's content is not like a text box where the user can type something that the code needs to know about. When you're binding to something that the user cannot change, forget about UpdateSourceTrigger, it's the Target property that counts.
The handler should mark the event. This is good practice and did not affect the binding.
The binding constructor needs only the path.
This code will give you your expected result; i.e., that the label updates when the button is clicked. Checked, compiled, and executed on vs2013, .net 4.5.
The other respondents said you should use a View Model. I agree with this 100%, and overall it's a good thing to consider.
You want to use a property to do this, as well as implementing INotifyPropertyChanged so that the label's content gets updated when the property changes.
Here's an example using a simple ViewModel
xaml:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:converters="clr-namespace:WpfApplication1"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
<Label Width="200" Height="50" Content="{Binding MyLabel}"/>
<Button Height="30" Width="100" Content="Increment" Click="Button_Click" />
</StackPanel>
</Window>
xaml.cs:
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
MainViewModel vm = new MainViewModel();
public MainWindow()
{
InitializeComponent();
this.DataContext = vm;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
vm.MyLabel += 1;
}
}
}
MainViewModel.cs:
namespace WpfApplication1
{
public class MainViewModel : INotifyPropertyChanged
{
#region Members
private int _myLabel;
#endregion Members
#region Properties
public int MyLabel
{
get
{
return _myLabel;
}
set
{
_myLabel = value;
NotifyPropertyChanged("MyLabel");
}
}
#endregion Properties
public MainViewModel()
{
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
}
Note: Ideally, you would want to use a Command for the Button instead of a Click event handler
You cannot bind to something that is private or a field so convert it into public property. You can find more as to what is a valid binding source here
If you want changes to your property be picked up by UI you should implement INotifyPropertyChanged interface and raise event each time value of the property changes. So idCounter should look more like this:
private int _idCounter;
public int idCounter
{
get { return _idCounter; }
set
{
if (_idCounter != value)
{
_idCounter = value;
OnPropertyChanged("idCounter");
}
}
}
When you create binding to property you use Path
Binding works in binding context so you need to specify from where to take this Path. Easiest way to do that is to set DataContext. So in your case initialization should look more like this:
Binding b = new Binding("idCounter");
b.Mode = BindingMode.OneWay;
b.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
myLabel.SetBinding(Label.ContentProperty, b);
DataContext = this;
As #d.moncada suggested in his answer you should create dedicated view model
Is it possible to make a window stay always on top even when other application is running on Fullscreen? I'm using right now TopMost = true but when other application is running on fullscreen mine becomes invisible. It's WindowStyle = None window by the way.
Edit: And do not let other window minimalize ofcourse
This won't work 100% of the time, but it will improve the situation somewhat. You can set Topmost = true in the handler for the Window.Deactivated event:
private void Window_Deactivated(object sender, EventArgs e)
{
Window window = (Window)sender;
window.Topmost = true;
}
The Deactivated event will be called whenever your application loses focus (often when another application requests to be Topmost) and so this will reset your application on top after this.
Try this solution from MSDN, it should work for you.
In the Window Activated Event add the following code:
this.Width = System.Windows.SystemParameters.PrimaryScreenWidth;
this.Height = System.Windows.SystemParameters.PrimaryScreenHeight;
this.Topmost = true;
this.Top = 0;
this.Left = 0;
in DeActivated Event add the following code
this.Topmost = true;
this.Activate();
Original post from MSDN
None of the solutions above for me worked, so here is what I ended up doing. It worked perfectly for me.
Basically, to keep it on top you just set the lose focus event to make it go back to top.
XAML:
PreviewLostKeyboardFocus="Window_PreviewLostKeyboardFocus"
Code Behind:
private void Window_PreviewLostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
var window = (Window)sender;
window.Topmost = true;
}
If you want your application to stay on top of EVERYTHING (including the start interface in Windows 8, previously known as "Metro"), then you can specify UiAccess="True" in your manifest file. This is typically used by accessibility applications such as onscreen keyboards.
From memory you need to do 3 things;
Request UiAccess="True"
Sign your application's exe file with a recognised certificate. I obtained a free code signing certificate from Certum as my project is Open Source.
Install your application to a "Trusted Location", which in my case was the program files directory. There is no official definition of "Trusted Location" that I could find.
So I ran into the same requirement recently. It seems the top rated answer as well as the second didn't properly work for me. I've found a solution that seems to work flawlessly and somewhat adheres to best practice using MVVM.
Using the below forces the window to the top and never lapses on change like the other solutions.
Step 1: I created a simple state manager class for my main client window. I used INotifyPropertyChanged to keep property in sync when using a direct binding to my window. (very important)
public class ClientStateManager : INotifyPropertyChanged
{
#region Private Variables
private bool isForceToTop;
private bool isClientEnabled;
#endregion
#region Public Properties
public bool IsForceToTop
{
get { return isForceToTop; }
set
{
isForceToTop = value;
NotifyPropertyChanged();
}
}
public bool IsClientEnabled
{
get { return isClientEnabled; }
set
{
isClientEnabled = value;
NotifyPropertyChanged();
}
}
#endregion
#region Private Methods
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
#region Public Methods
public void Lock() => this.IsClientEnabled = false;
public void UnLock() => this.IsClientEnabled = true;
public void SetTop() => this.IsForceToTop = true;
public void UnSetTop() => this.IsForceToTop = false;
#endregion
#region Public Events
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
Step 2.1: Added my state manager class to my ViewModel. (MVVM)
internal class MainWindowViewModel : INotifyPropertyChanged
{
#region Constructor
public MainWindowViewModel()
{
ClientStateManager = new ClientStateManager();
}
#endregion
#region Public Properties
public ClientStateManager ClientStateManager { get; private set; }
#endregion
}
Step 2.2: Then set your window data context to your view model.
private MainWindowViewModel model;
private MainWindow()
{
InitializeComponent();
this.model = new MainWindowViewModel();
this.DataContext = model;
}
Step 3: Add your data binding to your window.
<Window x:Class="Intouch_Work.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ojects="clr-namespace:Framework.Object;assembly=Framework"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
Title="Intouch" Height="800" Width="1100"
x:Name="mainWindow"
Topmost="{Binding Path=ClientStateManager.IsForceToTop, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}">
So now you can manage your window state using the state manager object initialized within the View Model. You can call SetTop() from you state manager to push it forward, or UnSetTop() to stop it.
Hope this helps anyone looking to do the same.
I needed something similar for an industrial quality control application, where non-admin operators should not be able to minimize or use anything else on the dedicated computer while the application is running, not even Windows+D to bring up the desktop. And I found out that the cleanest and simplest way to achieve that is through:
Settings the correct Width, Height, WindowStyle, WindowState and Topmost properties.
Handling some related events: StateChanged, Deactivated, LostFocuse, LostMouseCapture, LostKeyboardFocus and PreviewLostKeyboardFocus.
Handling closing events with ALT+F4 or a custom button).
No need for P/Invoke, Here is the full code:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Loaded += OnLoaded;
Closing += OnClosing;
StateChanged += OnStateChanged;
Deactivated += (sender, args) => Activate();
LostFocus += (sender, args) => Focus();
LostMouseCapture += (sender, args) => Mouse.Capture(this);
LostKeyboardFocus += (sender, args) => Keyboard.Focus(this);
PreviewLostKeyboardFocus += (sender, args) => Keyboard.Focus(this);
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
Width = SystemParameters.PrimaryScreenWidth;
Height = SystemParameters.PrimaryScreenHeight;
WindowStyle = WindowStyle.None;
WindowState = WindowState.Maximized;
Topmost = true;
// Other stuff here
}
private void OnClosing(object sender, CancelEventArgs e)
{
// You might want to allow only some users to close the app
if (MessageBox.Show("Are you an admin?", "Admin Check", MessageBoxButton.YesNo, MessageBoxImage.Warning) == MessageBoxResult.No)
e.Cancel = true;
}
private void OnStateChanged(object sender, EventArgs e)
{
if (WindowState == WindowState.Minimized)
WindowState = WindowState.Maximized;
}
}
You can also put some of this in XAML:
<Window x:Class="FullScreen.MainWindow"
...
Title="MainWindow"
WindowState="Maximized"
WindowStyle="None"
Topmost="True">
<Grid>
</Grid>
</Window>
I had a main window that I wanted to keep on top of everything (if the user checked "always on top".
This worked for me. Hope this helps someone.
// If we want main to stay on top, we set the rest of the menus to Not be top
if (mnuViewMainWindowAlwaysOnTopo.IsChecked)
{
this.Topmost = true;
foreach (var window in Application.Current.Windows)
// Don't change for main window
if (window.GetType().Name != this.GetType().Name)
window.Topmost = false;
}
else this.Topmost = false;
I have run into an issue with combo boxes in WPF where they seem to hang onto the first DataContext they were opened with. When I change the DataContext on my ComboBox, a child PopupRoot object still references the old DataContext.
At first I assumed we were doing something wrong but I was having trouble working out what that might be so I tried to simplify. I have managed to recreate the behavior I am seeing in our application in a very simple form so it seems more like a bug in the WPF ComboBox implementation. That sounds a little controversial so I thought I'd turn to stackoverflow for help.
The core code for the sample is below:
<Window x:Class="ComboBoxTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="150" Width="525">
<DockPanel>
<Button Click="ReloadModel" Width="137" Height="40">Reload Model</Button>
<ComboBox Name="ComboBox"
ItemsSource="{Binding AvailableOptions}"
SelectedItem="{Binding SelectedOption}"
Width="235" Height="43">
</ComboBox>
</DockPanel>
</Window>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var newModel = new ViewModel();
ComboBox.DataContext = newModel;
}
private void ReloadModel(object sender, RoutedEventArgs e)
{
var newModel = new ViewModel();
ComboBox.DataContext = newModel;
}
}
public class ViewModel : INotifyPropertyChanged
{
public ViewModel()
: this(new[] { "Option 1", "Option 2", "Option 3" })
{ }
public ViewModel(IEnumerable<string> options)
{
_selectedOption = options.First();
_availableOptions = new ObservableCollection<string>(options);
}
protected void RaisePropertyChanged(string propertyName)
{
var propertyChangedHandler = PropertyChanged;
if (propertyChangedHandler != null)
{
propertyChangedHandler(this, new PropertyChangedEventArgs(propertyName));
}
}
public event PropertyChangedEventHandler PropertyChanged;
private readonly ObservableCollection<string> _availableOptions;
public ObservableCollection<string> AvailableOptions
{
get
{
return _availableOptions;
}
}
private string _selectedOption;
public string SelectedOption
{
get { return _selectedOption; }
set
{
if (_selectedOption == value)
{
return;
}
_selectedOption = value;
RaisePropertyChanged("SelectedOption");
}
}
}
Steps to reproduce:
1) Run Application
2) Open Combobox (so that it renders the drop down options)
3) Click "Reload Model" button
At this point there will be be two ViewModel objects, the older, unexpected instance is rooted like:
ViewModel->PopupRoot->Popup->ComboBox->MainWindow->App
Is this a bug or am I doing it wrong?
Eamon
Joe's comment brought my attention back to this old question which I have solved for my own use. In the end I wrote a Behavior that I could attach to a combobox that dealt with the memory leak.
I've posted the code here: https://github.com/EamonHetherton/Demos/blob/master/StackOverflow/18096050/StopComboBoxMemoryLeakBehaviour.cs
caveat emptor: this solution relies on reflection and the fragility that could entail. It works for me, YMMV.
Recently I encountered several memory leak problems which were related to Popup / ContextMenu / ComboBox binding with DataContext.
I found out that essentially the problem for Popup / ComboBox was that the "_popupRoot"'s DataContext was not released after the DataContext of its parents were set to null.
For ContextMenu, if it's used with some kind of ItemsSource binding generated controls, then WPF will cache the Contextmenu, so its DataContext will not be released unless the user right click to pop up the ContextMenu somewhere again.
I managed to create 3 derived classes to replace the WPF controls where DataContext binding was used. I will paste them here, hopefully, they may be useful to someone else.
public class ComboBoxFixMem : ComboBox
{
public ComboBoxFixMem()
{
this.DataContextChanged += ComboBox_DataContextChanged;
}
private void ComboBox_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (this.DataContext != null)
return;
FrameworkElement fe = this.GetTemplateChild("PART_Popup") as FrameworkElement;
if (null != fe)
fe.DataContext = null;
PopupFixMem.ClearPopupDataContext(fe as Popup);
}
}
public class ContextMenuFixMem : ContextMenu
{
protected override void OnClosed(RoutedEventArgs e)
{
base.OnClosed(e);
FrameworkElement p = this.Parent as FrameworkElement;
if (null != p)
p.DataContext = null;
}
}
public class PopupFixMem : Popup
{
public PopupFixMem()
{
this.DataContextChanged += Popup_DataContextChanged;
}
private void Popup_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (this.DataContext != null)
return;
ClearPopupDataContext(this);
}
public static void ClearPopupDataContext(Popup popup)
{
if (null == popup)
return;
try
{
var fiPopupRoot = typeof(Popup).GetField("_popupRoot", BindingFlags.NonPublic | BindingFlags.Instance);
var popupRootWrapper = fiPopupRoot?.GetValue(popup);
if (null == popupRootWrapper)
return;
var valueFieldInfo = popupRootWrapper.GetType().GetProperty("Value", BindingFlags.NonPublic | BindingFlags.Instance);
var popupRoot = valueFieldInfo?.GetValue(popupRootWrapper, new object[0]) as FrameworkElement;
if (null != popupRoot)
popupRoot.DataContext = null;
}
catch (Exception) { }
}
}
I have a window that essentially runs a timer. When the timer hits 0 I want to bring the window to the front so that it is visible and not hidden behind some other application.
From what I can gather I would simply call window.activate() to accomplish this but with mvvm my view model doesn't have a reference to window.
A "purist" MVVM solution is to use a behavior. Below is a behavior for a Window with an Activated property. Setting the property to true will activate the window (and restore it if it is minimized):
public class ActivateBehavior : Behavior<Window> {
Boolean isActivated;
public static readonly DependencyProperty ActivatedProperty =
DependencyProperty.Register(
"Activated",
typeof(Boolean),
typeof(ActivateBehavior),
new PropertyMetadata(OnActivatedChanged)
);
public Boolean Activated {
get { return (Boolean) GetValue(ActivatedProperty); }
set { SetValue(ActivatedProperty, value); }
}
static void OnActivatedChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e) {
var behavior = (ActivateBehavior) dependencyObject;
if (!behavior.Activated || behavior.isActivated)
return;
// The Activated property is set to true but the Activated event (tracked by the
// isActivated field) hasn't been fired. Go ahead and activate the window.
if (behavior.AssociatedObject.WindowState == WindowState.Minimized)
behavior.AssociatedObject.WindowState = WindowState.Normal;
behavior.AssociatedObject.Activate();
}
protected override void OnAttached() {
AssociatedObject.Activated += OnActivated;
AssociatedObject.Deactivated += OnDeactivated;
}
protected override void OnDetaching() {
AssociatedObject.Activated -= OnActivated;
AssociatedObject.Deactivated -= OnDeactivated;
}
void OnActivated(Object sender, EventArgs eventArgs) {
this.isActivated = true;
Activated = true;
}
void OnDeactivated(Object sender, EventArgs eventArgs) {
this.isActivated = false;
Activated = false;
}
}
The behavior requires a reference to System.Windows.Interactivity.dll. Fortunately, this is now available on NuGet in the Blend.Interactivity.Wpf package.
The behavior is attached to a Window in XAML like this:
<Window ...>
<i:Interaction.Behaviors>
<Behaviors:ActivateBehavior Activated="{Binding Activated, Mode=TwoWay}"/>
</i:Interaction.Behaviors>
The view-model should expose a boolean Activated property. Setting this property to true will activate the window (unless it is already activated). As an added bonus it will also restore a minimized window.
You could go about it in a couple of ways - adding a reference to the window could work since the viewmodel is not coupled with the view but related to it, but I don't really like that approach since it pretty much does couple your view to your viewmodel - which is not really the point of MVVM
A better approach may be to have your viewmodel raise an event or a command which the view can handle. This way the view gets to decide what UI action is associated with the command/event
e.g. simply
class SomeView
{
void HandleSomeCommandOrEvent()
{
this.Activate();
}
}
Of course how you wire this up is up to you but I'd probably try and get routed commands happening
Edit: You can't really 'bind' a simple event, since it's invoked from the viewmodel.
A simple event based example is just to add the event to the viewmodel and handle it directly ... e.g. imagine the following MainWindow with a ViewModel property
public partial class MainWindow : Window
{
MainWindowViewModel ViewModel { get; set; }
public MainWindow()
{
InitializeComponent();
ViewModel = new MainWindowViewModel();
ViewModel.ShowMessage += ViewModel_ShowMessage;
this.DataContext = ViewModel;
}
void ViewModel_ShowMessage(object sender, ShowMessageEventArgs e)
{
MessageBox.Show(e.Message, "Some caption", MessageBoxButton.OK);
}
}
Then the ViewModel can just fire the event:
// The view model
public class MainWindowViewModel
{
// The button click command
public RelayCommand ButtonClickCommand { get; set; }
// The event to fire
public event EventHandler<ShowMessageEventArgs> ShowMessage;
public MainWindowViewModel()
{
ButtonClickCommand = new RelayCommand(ButtonClicked);
}
void ButtonClicked(object param)
{
// This button is wired up in the view as normal and fires the event
OnShowMessage("You clicked the button");
}
// Fire the event - it's up to the view to decide how to implement this event and show a message
void OnShowMessage(string message)
{
if (ShowMessage != null) ShowMessage(this, new ShowMessageEventArgs(message));
}
}
public class ShowMessageEventArgs : EventArgs
{
public string Message { get; private set; }
public ShowMessageEventArgs(string message)
{
Message = message;
}
}
The XAML would be:
<Button Command="{Binding ButtonClickCommand}">Click me!</Button>
So the button invokes the command, which in turn fires the event which the view (MainWindow) handles and shows a messagebox. This way the view/UI decides on the course of action based on the type of event raised. Of course it could be your timer which fired the event
You can always go down the more involved route such as some of the answers on this question...
How should the ViewModel close the form?
but to be honest, it depends if you really need it - a simple event works well - some people overcomplicate things for the sake of elegance, but at the detriment of simplicity and productivity!
I would go this way:
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using GalaSoft.MvvmLight.Messaging;
// View
public partial class TestActivateWindow : Window
{
public TestActivateWindow() {
InitializeComponent();
Messenger.Default.Register<ActivateWindowMsg>(this, (msg) => Activate());
}
}
// View Model
public class MainViewModel: ViewModelBase
{
ICommand _activateChildWindowCommand;
public ICommand ActivateChildWindowCommand {
get {
return _activateChildWindowCommand?? (_activateChildWindowCommand = new RelayCommand(() => {
Messenger.Default.Send(new ActivateWindowMsg());
}));
}
}
}
public class ActivateWindowMsg
{
}