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.
Related
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;
}
I'm trying to further customize build-in capability of WPF ListBox for showing items in groups.
In short, I want to hide Group's container (and Group's title altogether) if all items inside group are collapsed (Visibility property).
First, I have very simple class City that represent single Item. This class include Shown property. Inside ItemContainerStyle I simply have DataTrigger that set Visibility to Collapsed if value of this property is False.
class City : INotifyPropertyChanged
{
private bool m_Shown = true;
public string Name { get; set; }
public string Country { get; set; }
public bool Shown
{
get
{
return m_Shown;
}
set
{
m_Shown = value;
PropertyChanged.Invoke(this, new PropertyChangedEventArgs("Shown"));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
This is how I add sample cities, add Group description and all work fine.
m_cities = new List<City>
{
new City() { Name = "Berlin", Country = "Germany" },
new City() { Name = "Milano", Country = "Italy" },
new City() { Name = "Frankfurt", Country = "Germany" },
new City() { Name = "Rome", Country = "Italy" }
};
ICollectionView view = CollectionViewSource.GetDefaultView(m_cities);
view.GroupDescriptions.Add(new PropertyGroupDescription("Country"));
Cities = view; // <-- Binds to ItemsSource of ListBox
I tried in several ways to automatically hide Group if there are no more items visible in it (all are collapsed), but all without luck.
One way is to repeat last 3 lines in code above and this works, but I noticed slowdown with this method and listbox must work fast for user.
Bellow is one of my examples and this actually worked for hiding, but I can't bring group to be visible anymore after that. I tried with converters and similar, but I can't get group visible again.
<ListBox.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Style.Triggers>
<Trigger Property="ActualHeight" Value="20">
<Setter Property="Visibility" Value="Collapsed"/>
</Trigger>
</Style.Triggers>
<Setter Property="Visibility" Value="Visible"/>
<Setter Property="MinHeight" Value="20"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupItem}">
<StackPanel>
<TextBlock Text="{Binding Path=Name}"/>
<ItemsPresenter/>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</ListBox.GroupStyle>
Thanks for any help.
A bit (!) late with this, but hopefully it might help someone else in the future.
Inside the control template of most (all?) GroupItem styles is an ItemsPresenter that is used to host and display the child items that belong to the group. It stands to reason that, if all of the child items are collapsed, this ItemsPresenter will have a height of zero.
Therefore, you can add a trigger to the control template based on this condition, and set the Visibility of the whole group item accordingly. A normal property trigger doesn't seem to work, but a data trigger will. Something like this:
<ControlTemplate>
<StackPanel x:Name="Root">
...
<ItemsPresenter x:Name="ItemsPresenter" />
...
</StackPanel>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding ActualHeight, ElementName=ItemsPresenter}" Value="0">
<Setter TargetName="Root" Property="Visibility" Value="Collapsed" />
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
You will need to name the root element of the control template (in this example it's a StackPanel element I've named "Root") and also the ItemsPresenter element (I've just called it "ItemsPresenter"). Obviously the root element might be a different type, and you can use whichever names you like.
You were on the right track, but you needed to bind to the ActualHeight of the ItemsPresenter, and it needed to be a data trigger not a normal property trigger.
In our MVVM application, in a View, DataContext is initially null and is set later.
The View is first rendered without the DataContext set, so for bindings the default or FallbackValues are used. This causes a lot of changes in the UI once the DataContext is set and all bindings are updated. The UI is a bit 'bouncy' and I can imaging that quite a few CPU cycles are wasted.
Is there a way to postpone rendering of the View until the DataContext is set?
Our code to find a View for a ViewModel:
<ContentControl
DataContext="{Binding Viewodel}"
Content="{Binding}"
Template="{Binding Converter={converters:ViewModelToViewConverter}}"/>
ViewModelToViewConverter.cs:
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
ViewModel viewModel = value as ViewModel;
if (viewModel == null)
{
return null;
}
string modelName = viewModel.ToString();
string mappingId = viewModel.MappingId;
if (!string.IsNullOrEmpty(mappingId))
{
modelName += "_" + mappingId;
}
ControlTemplate controlTemplate = new ControlTemplate();
MappingEntry mappingEntry = ApplicationStore.SystemConfig.GetMappingEntryOnModelName(modelName); // lookup View definition for ViewModel
Type type = mappingEntry != null ? mappingEntry.ViewType : null;
if (type != null)
{
controlTemplate.VisualTree = new FrameworkElementFactory(type);
}
else
{
Logger.ErrorFormat("View not found: {0}", modelName);
}
return controlTemplate;
}
Yes, you can do that
Using FrameworkElement.DataContextChanged event.
Using Trigger.
Schematic sample eg;
<ContentControl>
<ContentControl.Resources>
<DataTemplate x:Key="MyTmplKey">
<TextBlock Text="Not null"/>
</DataTemplate>
<DataTemplate x:Key="DefaultTmplKey">
<StackPanel>
<TextBlock Text="null"/>
<Button Content="Press" Click="Button_Click_1"/>
</StackPanel>
</DataTemplate>
</ContentControl.Resources>
<ContentControl.Style>
<Style TargetType="ContentControl">
<Setter Property="ContentTemplate" Value="{StaticResource MyTmplKey}"/>
<Style.Triggers>
<Trigger Property="DataContext" Value="{x:Null}">
<Setter Property="ContentTemplate" Value="{StaticResource DefaultTmplKey}"/>
</Trigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
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();
}
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.