Open/Close ToolWindow from its DataContext (ViewModel)

Docking/MDI for WPF Forum

Posted 4 years ago by Eric P
Version: 15.1.0622
Avatar

I have installed the Actipro trial and I have been going through the samples and evaluating its capabilities. We are interested in a WPF docking control that can support an ItemsSource binding for the panels in the dock control. I have gone through the sample that shows how to setup the DockSite with bindings using its ToolItemsSource property.

I have a ToolItemViewModel that contains properties named DefaultDock, DockGroup, IsInitiallyAutoHidden, Description, ImageSource, Title, and Name. These properties were the same properties defined the in the MVVM sample for the DockSite. These properties all seem to be bound and working correctly. For example, when I change the Title property at runtime I can see the text at the top of the ToolWindow change.

Now I would like to be able to open the associated ToolWindow by changing a property in the ViewModel that is the DataContext for the ToolWindow. I saw that there was an IsOpen property on ToolWindow so I added an IsOpen boolean property to the ToolItemViewModel that contains the other notifiable properties. I also altered the ToolItemStyle that is defined in the Application Resources to the following.

<Style x:Key="ToolItemStyle" TargetType="docking:ToolWindow" BasedOn="{StaticResource DockingItemStyle}">
    <Setter Property="IsOpen" Value="{Binding IsOpen}" />
</Style>

I also added the same Setter for the IsOpen property to the Style for the DocumentWindow and DockingWindow just to be sure. When I change the IsOpen property at runtime it has no effect on the ToolWindow. I was not sure the binding was working so I deliberately misspelled the binding property just to see if I could get a Binding error and I did but only for the ToolItemStyle. So I only have the IsOpen property binding style for the ToolWindow (as shown above).

A little more info. The plan right now is to create all ViewModels that inherit from ToolItemViewModel at startup and add these VMs to the binding collection for the DockingSite ToolItemsSource. Each VM will have a default setting that controls if it should be open and pinned or simply closed and not visible. Using the EventAggregator from Prism, each VM will be listening to an event used to open and pin the ToolWindow. I thought I could simply change the IsOpen property to True and the ToolWindow would be opened and pinned.

What is the best way to open, close, and activate a ToolWindow from a bindable property in its DataContext ViewModel?

Thanks,

   -eric

Comments (13)

Answer - Posted 4 years ago by Actipro Software Support - Cleveland, OH, USA
Avatar

Hi Eric,

Try putting a Mode=TwoWay on that Binding to see if it helps.


Actipro Software Support

Posted 4 years ago by Eric P
Avatar

That did the trick! Thank you very much.

I am curous the Title property for the DockingWindow does not have Mode=TwoWay but when I change the Title property in my ViewModel it changes the Title property of the DockingWindow. What is different between DockingWindow.Title vs. ToolWindow.IsOpen?

Thanks,

   -eric

Posted 4 years ago by Actipro Software Support - Cleveland, OH, USA
Avatar

It might be that our code ends up programmatically updating IsOpen when appropriate based on layout changes and perhaps that breaks the binding if it's not TwoWay.


Actipro Software Support

Posted 4 years ago by Eric P
Avatar

So setting IsOpen = true will open the tab but it will not make it the active tab. I was hoping there would be an IsActive property that I could use but I do not see one. I see an Activate() method but as far as I know that can not be called from the ViewModel. Can you suggest a way to ensure the tab becomes open and active?

Posted 4 years ago by Actipro Software Support - Cleveland, OH, USA
Avatar

Hi Eric,

Sorry but there isn't a property you can bind to to get that to happen.  You would need to have your view model raise an event that your view would watch, then get the ToolWindow for that view model, and call Activate() on it.


Actipro Software Support

Posted 4 years ago by Eric P
Avatar

Thanks for the reply. Is this something that could be added in a future release? This seems like a must-have if someone is going to use binding and MVVM approach with the dock control. Activating a tab from the ViewModel is a requirement.

Raising an event in the ViewModel that the View listens to seem to violate MVVM (depending on how you look at it), which would be the separation of the ViewModel and the View. I really think you guys are going to need to add an IsActive property on your ToolWindow or the DockingWindow class.

I was thinking of trying a custom behavior to the dock control, like the samples do for what it calls a DockSiteViewModelBehavior. I can listen to the Activate and Deactivate events of the DockSite which lets me set the IsActive property on my ViewModel but I am having a problem going the other way. When the IsActive property changes in the ViewModel. I think the answer is to "attach" a custom Dependency Property to the ToolWindow (in code vs. in XAML) from DockingSiteViewModelBehavior.OnDockingSiteWindowRegistered method. I am not sure how to do that right now.

Posted 4 years ago by Actipro Software Support - Cleveland, OH, USA
Avatar

While our vNext version (currently work in progress) is adding an IsActive property, it is read-only.  I'm not sure that allowing a setter there is a good idea since that property is only supposed to be set true on a single docking window in the entire layout at a time.  Allowing a setter might end up causing some problem scenarios.

What if you added something like an ActiveItem property on DockSite that was a view model oriented property and you somehow tied it to ActiveWindow?  That would be similar to a Selector.SelectedItem scenario.  In vNext we will be looking to improve MVVM support.  We haven't started on that area yet but can look into this as well when we get into that feature area.


Actipro Software Support

Posted 4 years ago by Eric P
Avatar

I hear what you are saying about having only one DockingWindow's IsActive property being true at one time. With the current DockSite, as far as I can tell, when one DockingWindow becomes Active the rest become inactive and if that logic was just reflected in a get/set IsActive property you should be good. That being said, what ever logic is in place now that ensures there is only one active tab would have to be applied to the setter as well.

Anyway, I was able to write a custom behavior or dependency property (I am not sure exactly what their called) to achieve this now. Here is what I came up with so far.

public static class DockSiteHelper {

  public static bool? GetIsActive(DependencyObject obj) {
    return (bool?)obj.GetValue(IsActiveProperty);
  }

  public static void SetIsActive(DependencyObject obj, bool? value){
    obj.SetValue(IsActiveProperty, value);
  }

  public static readonly DependencyProperty IsActiveProperty = DependencyProperty.RegisterAttached(
      "IsActive", typeof(bool?), typeof(DockSiteHelper), new FrameworkpropertyMetadata(null, OnIsActivePropertyValueChanged));

  private static bool _didAttach = false;

  private static void OnIsActivePropertyValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
    DockingWindow dockingWindow = d as DockingWindow;
    if (dockingWindow == null) { return; }

    if ((bool)e.NewValue) { dockingWindow.Activate(); }

    if (_didAttach == false) {
      _didAttach = true;
      dockingWindow.DockSite.WindowActivated += DockSiteHelper.OnWindowActivated;
      dockingWindow.DockSite.WindowDeactivated += DockSiteHelper.OnWindowDeactivated;
    }
  }

  private static void OnWindowActivated(object sender, DockingWindowEventArgs e){
    ((ToolItemViewModel)e.Window.DataContext).IsActive = true;
  }

  private static void OnWindowDeactivated(object sender, DockingWindowEventArgs e){
    ((ToolItemViewModel)e.Window.DataContext).IsActive = false;
  }

}

So far this works and it’s not bad, maybe could use some improvement. I have written these kinds of things before. For example, I needed to bind to the SelectedText, CaretIndex, SelectionStart, and SelectionLength properties of a TextBox but these properties are not bind-able. With that scenario it’s just binding a ViewModel property to a custom Dependency Property against a Dependency Object (in this case the TextBox).

<TextBox … h:TextBoxHelper.SelectedText=”{Binding SelectedText}” … />

Now in the case of the DockSite I did not want to bind to the DockSite. I did not want the Dependency Object to be DockSite I wanted it to be a DockingWindow. It’s like trying to bind to a Tab within a TabControl but all the tabs are defined at runtime using the TabControl.ItemsSource property.

So since you guys already included a “behavior” called DockSiteViewModelBehavior I was able to use that to attach my custom Dependency Property at the moment each DockingWindow was accessed. So I added the following one line to DockSiteViewModelBehavior.OnDockSiteWindowRegistered.

private static void OnDockingSiteWindowRegistered(object sender, DockingWindowEventArgs e){

    dockingWindow.SetBinding(DockSiteHelper.IsActiveProperty, “IsActive”); 

}

Now just for completeness here is the XMAL from the sample that attached the DockSiteViewModelBehavior.

<docking:DockSite ….. f:DockSiteViewModelBehavior.IsManaged = “true” ….. ><docking:DockSite />

This seems like a lot of code/work to have in the project and to maintain over time. We are trying to write less code and this seems to be taking us in the other direction. If you guys are in the process of improving your MVVM support then this IsActive property is definitely something to look at.

[Modified 4 years ago]

Posted 4 years ago by Eric P
Avatar

I’ve run into another issue that I could really use some help with. We have many potential ToolWindows that could be docked in our application (top, bottom, left and right). When the app starts up the very first time there is only one ToolWindow that needs to be docked left by default and open, all the others are not open by default. When the user clicks its associated button on the ribbon it will open its respective ToolWindow. When these other ToolWindows are opened for the first time they need to open in a predefined location (left, bottom or right). After they are open the user can re-dock it somewhere else and when the app shuts down it will serialize the dockstie state and then use that state when the app is opened next time.


To achieve the layout we want I have the following XAML for the dockSite.

<docking:DockSite .... ToolItemsSource={Binding ToolItems}" .... >

  <docking:SplitContainer Orientation="Horizontal">
  
    <!-- ToolWindowContainer Left -->
    <docking:ToolWindowContainer x:Name="ToolWindowDockedLeft" />

    <docking:SplitContainer Orientation="Vertical">
      
      <!-- ToolWindowContainer Top -->
      <docking:ToolWindowContainer x:Name="ToolWindowDockedTop" />

      <!-- DocumentWindow -->
      <docking:Workspace>
        <docking:DocumentWindow Title="Map">
          <ContentControl Content="{Binding MapContent}" />
        </docking:DocumentWindow>
      </docking:Workspace>

      <!-- ToolWindowContainer Bottom -->
      <docking:ToolWindowContainer x:Name="ToolWindowDockedBottom" />

    </docking:SplitContainer>

    <!-- ToolWindowContainer Right -->
    <docking:ToolWindowContainer x:Name="ToolWindowDockedRight" />

  </docking:SplitContainer>

</docking:DockSite>

This give me place holders for the top, bottom, left and right locations with a document in the middle. Now in the DockSiteViewModelBehavior (from the samples) I have the following for the OpenDockingWindow() method.

private static void OpenDockingWindow(DockSite dockSIte, DockingWindow dockingWindow) {

  switch (((ToolItemViewModel)dockingWindow.DataContext).DefaultDock) {
    case Dock.Left:
      ((ToolWindowContainer)dockSite.FindName("ToolWindowDockLeft")).Items.Add(dockingWindow);
      break;
    case Dock.Right:
      ((ToolWindowContainer)dockSite.FindName("ToolWindowDockRight")).Items.Add(dockingWindow);
      break;
    case Dock.Top:
      ((ToolWindowContainer)dockSite.FindName("ToolWindowDockTop")).Items.Add(dockingWindow);
      break;
    case Dock.Bottom:
      ((ToolWindowContainer)dockSite.FindName("ToolWindowDockBottom")).Items.Add(dockingWindow);
      break;
  }
}

The goal here was to put each ToolWindow in a default location (top, bottom, left, or right) but NOT have it be open and visible to the user. Only after the user clicks a respective button on the Ribbon would its ToolWindow become open and visible to the user (by setting IsOpen = true in the ViewModel). Also the state of the DockSite needs to be serialized and applied at startup for the next session. The .Items.Add(dockingWindow) does add it to a default container location but it forces the dockingWindow.IsOpen to be true and thus the IsOpen Setter on my ViewModel is being called (changing from false to true) and all the ToolWindows are open.


So my question is how can I add the ToolWindows to a container to define its default location but not have it open and visible to the user? Using code (not XAML) how can I add a ToolWindow to a container but have ToolWindow.IsOpen = false, without adding it to the container and then setting IsOpen = false (this will call the Setter twice).


As a test I tried the following XAML.

<docking:ToolWindowContainer x:Name="ToolWindowDockedLeft" />
  <docking:ToolWindow Name=”Test” Title=”Test” IsOpen=False />
</docking:ToolWindowContainer>

This does put the ToolWindow in the correct container and does not show it to the user. My guess is that the ToolWindow.IsOpen was actually called twice, once (IsOpen=true) when added to the container and the second time when it’s set to false. Is that true? I was really hoping that the DockingWindow.State enumeration would have a Hidden value. Then I could set the state to Hidden and then add it to a container (in code), but there is no Hidden enum value.

[Modified 4 years ago]

Posted 4 years ago by Actipro Software Support - Cleveland, OH, USA
Avatar

Hello,

We don't recommend doing what you are trying to do here since you are accessing some API that is only meant for internal usage.  You shouldn't ever put empty ToolWindowContainers or unnecessary SplitContainers in your XAML.  Likewise, you should never put a x:Name on any container since they are completely transient and will be dynamically created/destroyed as layouts change.

Instead what you can do is use methods like ToolWindow.Dock and specify a dock target and direction.  So for instance, you could target the DockSite and specify right direction to dock it on the right of the entire DockSite.  Or if another tool window is already open, you could content dock it (attach) to the target tool window.  You could also choose to dock a tool window next to another tool window.  Those kinds of methods are the ones you should use.

All that being said, in the work we've done for the upcoming vNext, there are a whole bunch of handy new properties and even a callback that lets you customize where a window will show when you simply set IsOpen="true".  Check out this post for details on these upcoming features:

http://blog.actiprosoftware.com/post/2015/08/27/DockingMDI-vNext-MVVM-Default-Dock-Locations


Actipro Software Support

Posted 4 years ago by Eric P
Avatar

Ok, I am trying to apply your advice. As far as I understand it I think the logic would be the following.

  1. First try and find an existing ToolWindow that is currently docked in the same location as the incoming ToolWindow’s DefaultDock
  2. If a ToolWindow was found for step #1 then dock the incoming ToolWindow with that ToolWindow.
  3. If a ToolWindow was not found for step #1 then dock the incoming ToolWindow with the DockSite using the ToolWindow’s DefaultDock

I am trying to change the OpenDockingWindow method to something like the following (this is pseudo code).

private static void OpenDockingWindow(DockSite dockSite, DockingWindow dockingWindow){

  //Try and find an existing ToolWindow that is already docked.
  ToolWindow toolWindow = dockSite.ToolWindows.LastOrDefault(
                              x => x.Dock == ((ToolWindow)dockingWindow).DefaultDock);

  if (toolWindow != null){

    //Dock (attach) the incoming ToolWindow with an existing ToolWindow
    ((ToolWindow)dockingWindow).Dock(toolWindow, ((ToolWindow)dockingWindow).DefaultDock);
  } else {

    //Dock the incoming ToolWindow with the DockSite
    ((ToolWindow)dockingWindow).Dock(dockSite, ((ToolWindow)dockingWindow).DefaultDock);
  }
}

 Am I on the right track here? If so how can I find an existing ToolWindow (if any) to content dock (attach) the incoming ToolWindow with?

Posted 4 years ago by Eric P
Avatar

Ok so I have refined the code a little and I had to use the Tag property because I could not figure out what the current docking location was for an existing ToolWindow. This is what I have so far.

ToolWindow toolWindow = dockSite.ToolWindows.LastOrDefault(
     x => x.Tag != null && (Dock)x.Tag == ((ToolItemVeiwModel)dockingWindow.DataContext).DefaultDock);

if (toolWindow != null) {
  ((ToolWindow)dockingWindow).Dock(toolWindow, Direction.Content);
} else {
  ((ToolWindow)dockingWindow).Dock(dockSite, ((ToolItemViewModel)dockingWindow.DataContext).DefaultDock);
}

//dockingWindow.IsOpen = ((ToolItemViewModel)dockingWindow.DataContext).IsInitiallyOpen;
((ToolWindow)dockingWindow).Tag = ((ToolItemViewModel)dockingWindow.DataContext).DefaultDock; 

This does what I need to do (mostly) and I think it’s more in line with your suggestions. I forgot to mention that I removed 99% of the XAML I had before and now all I have is the following.

<docking:DockSite ... ToolItemsSource="{Binding ToolItems" ...>
  <docking:SplitContainer Orientation="Vertical">
    <docking:DocumentWindow Title="Map">
      <ContentControl Content="{Binding MapViewModel}" />
    </docking:DocumentWindow>
  </docking:SplitContainer>
</docking:DockSite>

Now the last issue is how to set the IsOpen property. Let’s say I have two ToolWindows (A and B) that need to be docked by default to the left and together. ToolWindow A (IsOpen=false) is not open by default and ToolWindow B (IsOpen=true) is open by default. When the code processes ToolWindow A it does correctly dock it with the DockSite on the Left and sets its IsOpen property to false. Next when it processes ToolWindow B (to be docked Left also) it finds that ToolWindow A is already docked Left and then tries to Dock ToolWindow B using ToolWindow A as its docking target (Direction.Content) and then throws an exception because its docking target (ToolWindow A) is not open (IsOpen=false).

The exception is “Value cannot be null. Parameter name: obj”
ActiproSoftware.Windows.Controls.Docking.DockSite.GetDockSite(DependencyObject obj)

So how can I dock two ToolWindows together where one is not open (IsOpen=false) and the other is open (IsOpen=true)?

Posted 4 years ago by Actipro Software Support - Cleveland, OH, USA
Avatar

Hi Eric,

There is no ToolWindow.Dock property (like in your pseudocode) but there is a GetDirectionRelativeToWorkspace() method that gives varied results based on the current state.  Be sure to read the docs on that method, and you probably want to ensure the window is open in the desired state too.

In your slimmed down XAML, you don't need the extra SplitContainer since there's only one child.  Also, you can't put a DocumentWindow in a SplitContainer.  It would need to be a ToolWindow and in a ToolWindowContainer instead.

For the last question, you can't target a closed tool window.  In that case, you'd need to dock it on the left side of the DockSite instead.  Or you could initially have both in your XAML and just close tool window A.  When it would reopen, it would already have the breadcrumb in place to know where to go to.

In vNext, this post talks about how we're adding a WindowGroupName property that will intelligently group tool windows together that have the same group name when you set IsOpen = true.  That helps eliminate a lot of this logic you are doing.

http://blog.actiprosoftware.com/post/2015/08/27/DockingMDI-vNext-MVVM-Default-Dock-Locations


Actipro Software Support

The latest build of this product (v2019.1 build 0683) was released 1 month ago, which was after the last post in this thread.

Add Comment

Please log in to a validated account to post comments.