0

I am using the AvalonDock component in my MVVM WPF application. In my XAML I have something like this:

        <xcad:DockingManager Name="_dockingManager" Margin="5" Grid.Row="2"
                         DataContext="{Binding DockingManagerViewModel}"
                         DocumentsSource="{Binding Documents}"
                         ActiveContent="{Binding Path=ActiveContent, Mode=TwoWay}"
                         AnchorablesSource="{Binding Anchorables}">

Now I want to react on layout changes. As shown in the XAML snippet above I have bound the DockingManager to the "DockingManagerViewModel". So I assume to handle the layout changes also in my view model. The main problem is that the docking manager offers a LayoutChanging and LayoutChanged event and I have no idea how to handle this in my view model. I guess I cannot bind these events to corresponding commands in my view model? Any idea what's the best approach to handle this?

For better understanding, what I want to achieve is the following: The user shows a "Properties" window and then drags the window from the right side to the left. After that, the user closes the "Properties" window and soon after the user decides to show the properties window again. In this case I want to bring back the window on the left side because this was the last location. So my idea was to store the last location in the view model during the layout change so that I can restore the location when the view is shown again.

Franz Gsell
  • 1,425
  • 2
  • 13
  • 22
  • What do you want to do when the layout changes? Should it affect only the view or the underlying data (the view model)? I guess you could have some code-behind in the former, or bind to a command in your viewmodel in the latter. – Corentin Pane Sep 18 '19 at 09:40
  • @CorentinPane: A user can show or hide a view during the lifetime of the application. If a user shows a view again after it was hidden before, the view should appear on the exact same position as before. Therefore I thought I can store this information in the viewmodel to retrieve it later on – Franz Gsell Sep 18 '19 at 10:57
  • I don't specifically know about the xcad:DockingManager, but why would you listen to LayoutChanged event to know if you should hide or show your view? How is the user actually gonna hide or show a view? By clicking on a button? – Corentin Pane Sep 18 '19 at 11:25
  • Add event handlers to the view and execute the view model commands from there? – mm8 Sep 18 '19 at 12:02
  • @CorentinPane: I have updated the description above to clarify my question – Franz Gsell Sep 18 '19 at 14:17
  • maybe it should not even be in your view model, but in some kind of settings stored only in your view. – Corentin Pane Sep 18 '19 at 15:28

2 Answers2

2

So you want to react to some events happening in the UI. First things first: if you want, in reaction, to change only your view/layout, you do not need ICommand and a simple event handler will do the trick. If you expect to change the underlying data (your view model) in reaction to that event, you can use an ICommand or an event handler, as described below.

Let's first define a simple view model for our MainWindow:

public class MyViewModel {
    /// <summary>
    /// Command that performs stuff.
    /// </summary>
    public ICommand MyCommand { get; private set; }

    public MyViewModel() {
        //Create the ICommand
        MyCommand = new RelayCommand(() => PerformStuff());
    }

    public void PerformStuff() {
        //Do stuff that affects your view model and model.
        //Do not do anything here that needs a reference to a view object, as this breaks MVVM.
        Console.WriteLine("Stuff performed in ViewModel.");
    }
}

This assumes that you have a RelayCommand implementation of the ICommand interface that lets you invoke Action delegates on Execute:

public class RelayCommand : ICommand {
    //Saved Action to invoke on Execute
    private Action _action;

    /// <summary>
    /// ICommand that always runs the passed <paramref name="action"/> when executing.
    /// </summary>
    /// <param name="action"></param>
    public RelayCommand(Action action) {
        _action = action;
    }

    #region ICommand
    public event EventHandler CanExecuteChanged;

    public bool CanExecute(object parameter) => true;

    public void Execute(object parameter) => _action.Invoke();
    #endregion
}

In your MainWindow.xaml, we define two Border objects: the first one operates on the view model through an event handler, the second one through the ICommand pattern:

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="*"></RowDefinition>
        <RowDefinition Height="*"></RowDefinition>
    </Grid.RowDefinitions>
    <!--Performs action on click through event handler-->
    <Border Grid.Row="0" MouseUp="Border_MouseUp" Background="Red"></Border>
    <!--Performs action on click through ICommand-->
    <Border Grid.Row="1" Background="Blue">
        <Border.InputBindings>
            <MouseBinding MouseAction="LeftClick" Command="{Binding MyCommand}"></MouseBinding>
        </Border.InputBindings>
    </Border>
</Grid>

In your MainWindow.xaml.cs, assign a view model object and define the event handler for mouse up event:

public partial class MainWindow : Window {
    public MainWindow() {
        InitializeComponent();
        DataContext = new MyViewModel(); ;
    }

    //Handles mouse up event on the first Border
    private void Border_MouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e) {
        //...
        //Do stuff that affects only your view here!
        //...
        //Now the stuff that affects the view model/model:
        ((MyViewModel)DataContext).PerformStuff();
    }
}

Clicking on any of the two Border objects will perfom stuff on your view model.

How to apply this to your specific control and event?

  • You can always use a custom event handler DockingManager_LayoutChanged as shown above.

  • If you want to use the ICommand and your event is something else than a mouse or keyboard event, you can achieve the binding by following this instead of using a MouseBinding.

Corentin Pane
  • 4,794
  • 1
  • 12
  • 29
1

For such scenarios I always write attached properties.

For example for the Loaded-Event of a Window I use the following attached property:

internal class WindowExtensions
{
    public static readonly DependencyProperty WindowLoadedCommandProperty = DependencyProperty.RegisterAttached(
        "WindowLoadedCommand", typeof(ICommand), typeof(WindowExtensions), new PropertyMetadata(default(ICommand), OnWindowLoadedCommandChanged));

    private static void OnWindowLoadedCommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        Window window = d as Window;
        if (window == null)
            return;

        if (e.NewValue is bool newValue)
        {
            if (newValue)
            {
                window.Loaded += WindowOnLoaded;
            }
        }
    }

    private static void WindowOnLoaded(object sender, RoutedEventArgs e)
    {
        Window window = sender as Window;
        if (window == null)
            return;

        ICommand command = GetWindowLoadedCommand(window);
        command.Execute(null);
    }

    public static void SetWindowLoadedCommand(DependencyObject element, ICommand value)
    {
        element.SetValue(WindowLoadedCommandProperty, value);
    }

    public static ICommand GetWindowLoadedCommand(DependencyObject element)
    {
        return (ICommand) element.GetValue(WindowLoadedCommandProperty);
    }
}

In the viewmodel you have a standard command like:

private ICommand loadedCommand;
public ICommand LoadedCommand
{
    get { return loadedCommand ?? (loadedCommand = new RelayCommand(Loaded)); }
}

private void Loaded(object obj)
{
    // Logic here
}

And on the Window-Element in the XAML you write:

local:WindowExtensions.WindowLoadedCommand="{Binding LoadedCommand}"

local is the namespace where the WindowExtensions is located

Tomtom
  • 9,087
  • 7
  • 52
  • 95