WPF - AvalonDock - Closing Document - c#

I use AvalonDock with MVVM in a WPF project.
When I hit the "X" (Close button of the tab) my document closes but stays in memory. It seems that it is only hidden. It is not removed from my Model.Documents collection.
If I add DockingManager_DocumentClosing and try to remove my document from the collection, I receive an Exception in the following method of Xceed.Wpf.AvalonDock.Layout.LayoutContent because parentAsContainer is null.
/// <summary>
/// Close the content
/// </summary>
/// <remarks>Please note that usually the anchorable is only hidden (not closed). By default when user click the X button it only hides the content.</remarks>
public void Close()
{
var root = Root;
var parentAsContainer = Parent as ILayoutContainer;
parentAsContainer.RemoveChild(this);
if (root != null)
root.CollectGarbage();
OnClosed();
}
Does anybody know how I could manage document in AvalonDock that can be removed from my Model.Documents in order to be eventually be disposed when I hit its Close button?
For reference: This is my XAML of the AvalonDock:
<avalonDock:DockingManager
x:Name="DockingManager"
DocumentsSource="{Binding DocumentItems}"
ActiveContent="{Binding ActiveMainWindowViewModel,
Converter={StaticResource RestrictedClassConverter},
ConverterParameter={x:Type multiSimAnalysis:MainWindowViewModel},
Mode=TwoWay}"
DocumentClosing="DockingManager_DocumentClosing"
ActiveContentChanged="DockingManager_ActiveContentChanged">
<avalonDock:DockingManager.LayoutItemContainerStyleSelector>
<pane:PanesStyleSelector>
<pane:PanesStyleSelector.MainWindowViewLcStyle>
<Style TargetType="{x:Type avalonDock:LayoutItem}">
<Setter Property="Title" Value="{Binding Model.Title}"/>
<Setter Property="ToolTip" Value="{Binding Model.Title}"/>
<Setter Property="CloseCommand" Value="{Binding Model.CloseCommand}"/>
<Setter Property="IconSource" Value="{Binding Model.IconSource}"/>
<Setter Property="IsSelected" Value="{Binding Model.IsSelected, Mode=TwoWay}"/>
<Setter Property="IsActive" Value="{Binding Model.IsActive, Mode=TwoWay}"/>
<Setter Property="ContentId" Value="{Binding Model.ContentId}"/>
</Style>
</pane:PanesStyleSelector.MainWindowViewLcStyle>
</pane:PanesStyleSelector>
</avalonDock:DockingManager.LayoutItemContainerStyleSelector>
<avalonDock:DockingManager.LayoutItemTemplateSelector>
<multiSimAnalysis:PanesTemplateSelector>
<multiSimAnalysis:PanesTemplateSelector.MainWindowLcTemplate>
<DataTemplate>
<multiSimAnalysis:MainWindowViewLc />
</DataTemplate>
</multiSimAnalysis:PanesTemplateSelector.MainWindowLcTemplate>
</multiSimAnalysis:PanesTemplateSelector>
</avalonDock:DockingManager.LayoutItemTemplateSelector>
<avalonDock:DockingManager.Theme>
<avalonDock:VS2010Theme/>
</avalonDock:DockingManager.Theme>
<avalonDock:LayoutRoot>
<avalonDock:LayoutPanel Orientation="Horizontal">
<avalonDock:LayoutAnchorablePane DockWidth="400">
<avalonDock:LayoutAnchorable Title="Scope(s) selection" x:Name="PanelScopeSelection" IsVisible="True">
<scopeSelection:UserControlSelectStudyScope x:Name="ToolScopeSelection"/>
</avalonDock:LayoutAnchorable>
</avalonDock:LayoutAnchorablePane>
<avalonDock:LayoutDocumentPane/>
<avalonDock:LayoutAnchorablePane DockWidth="150">
<avalonDock:LayoutAnchorable Title="Properties" x:Name="PanelScopePropertyGrid">
<!--<multiSimAnalysis:UserControlPropertyGrid x:Name="ToolPropertyGrid" />-->
<xctk:PropertyGrid x:Name="ToolPropertyGrid" SelectedObject="{Binding ActiveObject}" />
</avalonDock:LayoutAnchorable>
</avalonDock:LayoutAnchorablePane>
</avalonDock:LayoutPanel>
</avalonDock:LayoutRoot>
</avalonDock:DockingManager>

I actually find an unacceptable workaround.
It is really twisted.
I only give that as reference. There should be a clean way to do it.
// ************************************************************************
private void DockingManager_DocumentClosing(object sender, Xceed.Wpf.AvalonDock.DocumentClosingEventArgs e)
{
e.Document.CanClose = false;
DocumentModel documentModel = e.Document.Content as DocumentModel;
if (documentModel != null)
{
Dispatcher.BeginInvoke(new Action(() => this.Model.DocumentItems.Remove(documentModel)), DispatcherPriority.Background);
}
}

I have found that on a LayoutDocument or a LayoutAnchorablePane, applying both this setting works: CanClose="False" or CanFloat="False".
It removes the Close button.
<avalonDock:LayoutDocument Title="Board"
ContentId="Board"
CanClose="False"
CanFloat="False">
</avalonDock:LayoutDocument>

Register for IsVisibleChanged.
void layoutFPR_Hidden(object sender, EventArgs e)
{
LayoutAnchorable window = (LayoutAnchorable)sender;
YourClass content = window.Content as YourClass;
// Close the object
content = null;
((LayoutAnchorable)sender).Close();
}

Related

How to access element defined in DataGrid RowHeaderTemplate or DataGrid.RowHeaderSyle

I the following code
<DataGrid.RowHeaderTemplate >
<DataTemplate>
<CheckBox x:Name="SelectedItemCheckBox"
Margin="5 0 0 0"
IsChecked="{Binding Path=IsSelected,
Mode=TwoWay,
RelativeSource={RelativeSource FindAncestor,AncestorType={x:Type DataGridRow}}}">
</CheckBox>
</DataTemplate>
</DataGrid.RowHeaderTemplate>
or
<DataGrid.RowHeaderStyle>
<Style TargetType="{x:Type DataGridRowHeader}">
<Setter Property="Background" Value="White"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type DataGridRowHeader}">
<CheckBox x:Name="SelectedItemCheckBox"
Margin="5 0 0 0"
IsChecked="{Binding Path=IsSelected,
Mode=TwoWay,
RelativeSource={RelativeSource FindAncestor,AncestorType={x:Type DataGridRow}}}">
</CheckBox>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</DataGrid.RowHeaderStyle>
How can I access the SelectedItemCheckBox from code behind when row is selected ?
What I have so far:
private CheckBox GetCheckbox(int index)
{
DataGridRow row = (DataGridRow)MyGrid.ItemContainerGenerator.ContainerFromIndex(index);
//how do I get to that checkbox here ?
}
The ItemSource of MyGrid is set in code behind, normally I would access the cell by accessing MyGrid.Columns[] however this is a row header and it's not part of Columns[].
Please note that there are many rows with this checkbox defined depending the ItemSource size.
Also I wold like to know if there is a way of accessing the checkbox without changing the xaml and using it as it is.
If you want to access the row header's checkbox in your code-behind (and not use binding), you can "travel" the visual tree of your selected DataGridRow to find the header.
Add SelectionChanged event handler to the DataGrid:
<DataGrid x:Name="Grid" Loaded="Grid_Loaded" SelectionChanged="Grid_SelectionChanged">
Then in code-behind:
Get the selected row
Use VisualTreeHelper to find the header's checkbox
Do your magic
private void Grid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var item = (DataGridRow)Grid.ItemContainerGenerator.ContainerFromItem(e.AddedItems[0]);
var control = FindChild<CheckBox>(item, "SelectedItemCheckBox");
control.IsChecked = true;
}
For FindChild, there's multiple options available in here: How can I find WPF controls by name or type?
I used the following in this example: How can I find WPF controls by name or type?
public static T FindChild<T>(DependencyObject depObj, string childName)
where T : DependencyObject
{
// Confirm obj is valid.
if (depObj == null) return null;
// success case
if (depObj is T && ((FrameworkElement)depObj).Name == childName)
return depObj as T;
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
//DFS
T obj = FindChild<T>(child, childName);
if (obj != null)
return obj;
}
return null;
}

Getting binding expressions from code behind from Datatemplate Triggers

I've been set to maintain a wpf application where there is a listbox for logging purposes.
The items displayed using listbox are of type TextMessage, i.e. the listbox is bound to these text messages via
ObservableCollection<TextMessage> Messages;
listBox.DataContext = Messages;
Messages are then added with something like
Messages.Add(new TextMessage("Test", TypeOfMessage.Headline));
This is the definition of the class TextMessage
public enum TypeOfMessage
{
Normal,
Headline,
Focus,
Important,
Fail,
Success
}
public class TextMessage
{
public TextMessage(string content, TypeOfMessage typeOfMessage)
{
Content = content;
TypeOfMessage = typeOfMessage;
CreationTime = DateTime.Now;
}
public string Content { get; }
public TypeOfMessage TypeOfMessage { get; }
public DateTime CreationTime { get; }
}
The xaml definition for the listbox is something like this:
<ListBox x:Name="listBox" HorizontalAlignment="Left" Height="196" Margin="101,77,0,0" VerticalAlignment="Top" Width="256" ItemsSource="{Binding}" SelectionMode="Multiple">
<ListBox.InputBindings>
<KeyBinding
Key="C"
Modifiers="Control"
Command="Copy"
/>
</ListBox.InputBindings>
<ListBox.CommandBindings>
<CommandBinding
Command="Copy"
Executed="DoPerformCopy"
/>
</ListBox.CommandBindings>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock x:Name="TextToShow" Text="{Binding Content}"></TextBlock>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding TypeOfMessage}" Value="Normal">
<Setter TargetName="TextToShow" Property="Foreground" Value="Black"/>
</DataTrigger>
<DataTrigger Binding="{Binding TypeOfMessage}" Value="Focus">
<Setter TargetName="TextToShow" Property="Foreground" Value="Black"/>
<Setter TargetName="TextToShow" Property="FontWeight" Value="Bold"/>
</DataTrigger>
<DataTrigger Binding="{Binding TypeOfMessage}" Value="Headline">
<Setter TargetName="TextToShow" Property="Foreground" Value="RoyalBlue"/>
<Setter TargetName="TextToShow" Property="FontWeight" Value="Bold"/>
</DataTrigger>
<DataTrigger Binding="{Binding TypeOfMessage}" Value="Important">
<Setter TargetName="TextToShow" Property="Foreground" Value="Red"/>
</DataTrigger>
<DataTrigger Binding="{Binding TypeOfMessage}" Value="Fail">
<Setter TargetName="TextToShow" Property="Foreground" Value="Red"/>
<Setter TargetName="TextToShow" Property="FontWeight" Value="Bold"/>
</DataTrigger>
<DataTrigger Binding="{Binding TypeOfMessage}" Value="Success">
<Setter TargetName="TextToShow" Property="Foreground" Value="Green"/>
<Setter TargetName="TextToShow" Property="FontWeight" Value="Bold"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
This works nicely (i.e messages are displayed in the listbox in different font weight and color depending on their type), but now for the question :
Is there any way using BindingExpression or any other means to get the font formatting and coloring from code behind from the xaml definitions ?
The reason is that I want to just have the formatting in one place (just in the xaml as it is right now) but still be able to reuse it when I want to copy the contents (using code behind) including font formatting to the clipboard.
Example:
private void DoPerformCopy()
{
RichTextBox rtb = new RichTextBox();
foreach (TextMessage message in (listBox as ListBox)?.SelectedItems.Cast<TextMessage>().ToList())
{
TextPointer startPos = rtb.CaretPosition;
rtb.AppendText(message.Content);
rtb.Selection.Select(startPos, rtb.CaretPosition.DocumentEnd);
//
// Here it would be very nice to instead having multiple switch statements to get the formatting for the
// TypeOfMessage from the xaml file.
SolidColorBrush scb = new SolidColorBrush(message.TypeOfMessage == TypeOfMessage.Fail ? Colors.Red);
//
rtb.Selection.ApplyPropertyValue(RichTextBox.ForegroundProperty, scb);
}
// Now copy the whole thing to the Clipboard
rtb.Selection.Select(rtb.Document.ContentStart, rtb.Document.ContentEnd);
rtb.Copy();
}
Since I'm new to wpf, I'd really appreciate if someone has a tip for solving this. (I've tried hard to find an solution here at stackoverflow, but so far I've been unsuccessful)
Thanks in advance,
King regards
Magnus
Make a ContentPresenter with Content set to your TextMessage. Set the ContentTemplate to listBox.ItemTemplate and apply the template. It will create the visuals (TextBlock in this case). Then, just parse off the values from the TextBlock.
Also, your RichTextBox selection code wasn't working quite right so I fixed that by just inserting TextRanges to the end of it instead of trying to get the selection right.
private void DoPerformCopy(object sender, EventArgs e)
{
RichTextBox rtb = new RichTextBox();
foreach (TextMessage message in (listBox as ListBox)?.SelectedItems.Cast<TextMessage>().ToList())
{
ContentPresenter cp = new ContentPresenter();
cp.Content = message;
cp.ContentTemplate = listBox.ItemTemplate;
cp.ApplyTemplate();
var tb = VisualTreeHelper.GetChild(cp, 0) as TextBlock;
var fg = tb.Foreground;
var fw = tb.FontWeight;
var tr = new TextRange(rtb.Document.ContentEnd, rtb.Document.ContentEnd);
tr.Text = message.Content;
tr.ApplyPropertyValue(RichTextBox.ForegroundProperty, fg);
tr.ApplyPropertyValue(RichTextBox.FontWeightProperty, fw);
}
// Now copy the whole thing to the Clipboard
rtb.Selection.Select(rtb.Document.ContentStart, rtb.Document.ContentEnd);
rtb.Copy();
}

Load and save layout of anchorables - Binding of Visibility

I am facing the problem that I cannot open an anchorable of type X after I have loaded my old layout. This happens only when I have closed the anchorable of type X before saving the layout.
Does anyone have a similar problem with AvalonDock? Is this a bug of AvalonDock? After years of debugging, I fear that the binding <Setter Property="IsActive" Value="{Binding Model.IsActive, Mode=TwoWay}"/> doesn't get updated correctly in the view, when changing IsActive in the ViewModel. AvalonDock should be responsible for this task. But maybe the problem is the loading and saving of the layout?
The code
View
I am loading the saved layout of my anchorables (= tool windows) in the Loaded Event of my DockingManager in my View like this (simplified):
string savedLayout = Properties.Settings.Default.Layout;
XmlDocument doc = new XmlDocument();
doc.LoadXml(savedLayout);
// very simplified code. load saved xml layout and add anchorables to the dockmanager
doc.SelectNodes("//LayoutAnchorable").OfType<XmlNode>().ToList().ForEach(anchorable =>
{
this.DockManager.AnchorablesSource.Add(anchorable);
});
I am saving the current layout of my anchorables in the Closing Event of my MainWindow in my View like this (simplified):
XmlDocument doc = new XmlDocument();
XmlLayoutSerializer xmlLayoutSerializer = new XmlLayoutSerializer(this.DockManager);
using (MemoryStream stream = new MemoryStream())
{
xmlLayoutSerializer.Serialize(stream);
stream.Seek(0, SeekOrigin.Begin);
doc.Load(stream);
}
// here happens some magic. i think this code is not responsible for my problem
Properties.Settings.Default.Layout = doc.OuterXml;
The ViewModel is bound to the ViewModel in the XAML like this (simplified):
<xcad:DockingManager x:Name="DockManager" AnchorablesSource="{Binding Tools}" Loaded="DockManager_Loaded">
<xcad:DockingManager.LayoutItemContainerStyle>
<Style TargetType="{x:Type dockctrl:LayoutItem}">
<Setter Property="Title" Value="{Binding Model.ContentId}" />
<Setter Property="IsSelected" Value="{Binding Model.IsSelected, Mode=TwoWay}" />
<Setter Property="CanClose" Value="{Binding Model.CanClose, Mode=TwoWay}" />
<Setter Property="Visibility" Value="{Binding Model.IsVisible, Mode=TwoWay, Converter={StaticResource Bool2vis}, ConverterParameter={x:Static Visibility.Hidden}}"/>
<Setter Property="CloseCommand" Value="{Binding Model.CloseCommand}" />
<Setter Property="IconSource" Value="{Binding Model.IconSource}" />
<Setter Property="IsActive" Value="{Binding Model.IsActive, Mode=TwoWay}"/>
<Setter Property="ContentId" Value="{Binding Model.ContentId}" />
</Style>
</xcad:DockingManager.LayoutItemContainerStyle>
[...]
ViewModel
The anchorable is opened in the ViewModel of the MainWindow. Here is the example code for the messages:
public ObservableCollection<ToolBoxViewModelBase> Tools { get; } = new ObservableCollection<ToolBoxViewModelBase>();
public MainWindowViewModel()
{
// [...]
this.MessagesWindow = new MessagesWindowViewModel();
SimpleIoc.Default.Register<MessagesWindowViewModel>(() => this.MessagesWindow);
this.ShowMessagesWindowCommand = new RelayCommand(() => this.OpenToolBox(this.MessagesWindow));
// [...]
}
public void OpenToolBox<T>(T viewModel) where T : ToolBoxViewModelBase
{
// [...]
viewModel.IsVisible = true;
viewModel.IsActive = true;
// [...]
}
Just let me know if you need more information or wether i have missed to add some code!
Maybe I misunderstood you question but... IsActive property is not used for opening a tool saved into the layout. That property is used to set a Tool as active (focused). In order to open the tool saved into the layout you should handle the layoutSerializer_LayoutSerializationCallback attached the
Something like this:
var layoutSerializer = new XmlLayoutSerializer(this.DockManager);
layoutSerializer.LayoutSerializationCallback += layoutSerializer_LayoutSerializationCallback;
protected virtual void layoutSerializer_LayoutSerializationCallback(object sender, LayoutSerializationCallbackEventArgs e)
{
try
{
var model = this.Docs.Union(this.Tools).FirstOrDefault(vm => vm.ContentId == e.Model.ContentId);
if (model != null)
{
e.Content = model;
}
else
{
// Log load layout error info
}
}
catch (Exception ex)
{
// Log load layout error info
}
}

Deselect selected item in wpf tree view (MVVM)

I am using a WPF treeview, when i click on a node\item once it gets selected. When the user clicks on the selected node the second time i want this node\item to get deselected i.e. i should be able to get the event. IsSelected is not called if i click on the selected node\item that is already selected. How do i get it to work?
<TreeView Grid.Column="0" Grid.Row="1" ItemsSource="{Binding source}" Name="mytreeview">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
<Setter Property="FontWeight" Value="Normal" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="FontWeight" Value="Bold" />
</Trigger>
</Style.Triggers>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding displaytext}"/>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
and in my view model i have
public bool IsSelected
{
get
{
return _isSelected;
}
set
{
if (value != _isSelected)
{
_isSelected = value;
if (_isSelected)
{
//my logic
}
this.OnPropertyChanged("IsSelected");
}
}
}
if (value != _isSelected)
Assuming that the UI is even trying to set something, that line is blocking your toggle logic. Something like this should fix at least that part.
set
{
if (value != _isSelected)
{
_isSelected = value;
this.OnPropertyChanged("IsSelected");
}
else if(_isSelected)
{
IsSelected = false;
}
}
Otherwise the UI is checking the selection before setting the value and you'll need to handle it through some other user interaction like handling deselection on click.
I know this is a bit late but I've recently had the same requirement (i.e. unselecting a selected TreeViewItem on the second click) and I solved it by declaring an event handler for the 'MouseLeftButtonUp' event in a 'Style' entry for the ItemContainerStyle of the TreeView as follows:
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<EventSetter Event="MouseLeftButtonUp" Handler="TreeViewItem_MouseLeftButtonUp"/>
</Style>
</TreeView.ItemContainerStyle>
The event handler in the code behind was as follows:
private TreeViewItem prevTVI;
private void TreeViewItem_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
TreeViewItem tvi = (TreeViewItem)sender;
if (tvi == this.prevTVI)
{
this.prevTVI = null;
if (tvi.IsSelected)
tvi.IsSelected = false;
}
else
this.prevTVI = tvi;
e.Handled = true;
}
Now, I would like to ask if anyone thinks this approach breaks the MVVM pattern? I personally don't think so as the event handler is only concerned with the View and its objects not anything else but I would like to hear what others have to say, especially if someone has an alternative.
The IsSelected property is only changed when you select a new item. Clicking on the same item twice will normally have no effect. You would need to register the MouseDown event on the TreeView, and then force the item to be deselected in the code-behind.

Get an instance of TabItem's ContentTemplate

I have a TabControl tied to a collection of items where each item is supposed to be represented by a normal TabItem which hosts a user control, like so:
<TabControl x:Name="Items"
ItemsSource="{Binding ElementName=This,Path=Files}">
<TabControl.ItemContainerStyle>
<Style TargetType="{x:Type TabItem}">
<Setter Property="Header" Value="{Binding Path=Name}" />
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate DataType="{x:Type App:MyContext}">
<App:Task x:Name="task" Image="{Binding Path=Image}" />
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</TabControl.ItemContainerStyle>
</TabControl>
The ItemsSource is bound to an ObservableCollection<MyContext>.
I would like to get to each and every App:Task generated for each of my MyContext instances like so (or similar):
foreach (var file in Files)
{
var container = Items.ItemContainerGenerator.ContainerFromItem(file) as TabItem;
if (container == null) continue;
var task = container.Content as Task;
if (task == null) return;
// ...
}
But the container.Content is MyContext not Task. So I figured I should use:
var task = container.ContentTemplate.FindName("task") as Task;
But this throws an exception because at this point the ContentTemplate does not seem to have been applied yet. How can I force it or get what I want in any other way?
Why do you need the UserControl in the first place?
If you need to access something you haven't bound enough properties on your items to the UserControls.

Categories

Resources