-1

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.

Azzarrel
  • 537
  • 5
  • 20
  • There is nothing wrong with handling your mouse down even and passing a parameter to your command from your mouse down event. MVVM will still be valid – Nawed Nabi Zada Aug 30 '18 at 09:26
  • I've tried it with the MouseDown-event, but it either overrides the MouseDown-Event of my Controls or quite the opposite. However, the event didn't fire half the time. How to avoid that? – Azzarrel Aug 30 '18 at 09:50
  • Please show the code of what you have done with the mouse down event – Nawed Nabi Zada Aug 30 '18 at 10:02
  • I added the required code. My controls don't have a MouseDown event, but they might contain some thrid-party UI-elements which might use it. – Azzarrel Aug 30 '18 at 11:17
  • 9 out of 10 times you don't need buttons to be controls, if you keep them as they should be then handling of the click event will be a lot easier. Why do you need them to be `Controls`? – XAMlMAX Aug 30 '18 at 12:16

1 Answers1

1

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.

Nawed Nabi Zada
  • 2,819
  • 5
  • 29
  • 40