While working on a particular project, I used TypeListView custom control(i.e. TypeListView : ListView) with expander as shown below:
<DockPanel x:Name="MyDockPanel" Grid.Column="0" Grid.Row="1" Margin="0,0,0,0" HorizontalAlignment="Stretch" DataContext="{Binding Path=MainViewModel.TypeSelectionViewModel}">
<res:TypesListView x:Name="TypeListView"
BorderThickness="0,1,0,0"
ItemsSource="{Binding Path=Types}"
SelectedItem="{Binding Path=SelectedType, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectionChanged="TypeListView_SelectionChanged"
DockPanel.Dock="Left"
SelectionMode="Extended"
IsSynchronizedWithCurrentItem="True"
HorizontalAlignment="Stretch"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto"
HorizontalContentAlignment="Stretch"
ContextMenu="{StaticResource ListViewContextMenu}"
ItemContainerStyle="{StaticResource HeaderedListViewItemStyle}"
dragDrop:DragDrop.IsDragSource="True" >
<ListView.ItemTemplate>
<DataTemplate DataType="{x:Type vm:ListViewItemViewModel }">
<!--<Expander HorizontalContentAlignment="Stretch" HorizontalAlignment="Stretch">-->
<Expander BorderThickness="0"
HorizontalAlignment="Stretch"
Header="{Binding}"
Style="{StaticResource ExpanderStyle}"
IsExpanded="{Binding Path=IsExpanded, Mode=TwoWay}" >
<res:TypesListView BorderThickness="0"
ItemsSource="{Binding Variants}"
ItemContainerStyle="{StaticResource ExpandedListViewItemStyle}"
dragDrop:DragDrop.IsDragSource="True"
SelectedItem="{Binding Path=TypeSelectionViewModel.SelectedType, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<ListView.ItemTemplate>
<DataTemplate>
<DockPanel Margin="20,0,0,0" HorizontalAlignment="Stretch"
ToolTip="{Binding Path=ConsistencyState, Converter={StaticResource ConsistencyStateToToolTipConverter}}">
<Image Margin="0,0" Source="{Binding Icon}"/>
<TextBlock Margin="3,0" Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}"
Foreground="{Binding Path=ConsistencyState, Converter={StaticResource ConsistencyStateToColorConverter}}"/>
<Image Opacity="0.75" HorizontalAlignment="Right" Margin="0,0"
Source="{Binding Path=TypeSelectionViewModel.MainViewModel.DragDropIcon}"
ToolTip="{x:Static res:TextLibrary.TEXT_DRAGCOPYCMT}">
<Image.Visibility>
<MultiBinding Converter="{StaticResource DragDropIconVisibilityConverter}">
<Binding Path="TypeSelectionViewModel.SelectedType" />
<Binding Path="." />
</MultiBinding>
</Image.Visibility>
</Image>
</DockPanel>
</DataTemplate>
</ListView.ItemTemplate>
</res:TypesListView>
<!--<DataTemplate >-->
<!--<DockPanel HorizontalAlignment="Stretch"
ToolTip="{Binding Path=ConsistencyState, Converter={StaticResource ConsistencyStateToToolTipConverter}}">
<Image Margin="0,0" Source="{Binding Icon}"/>
<TextBlock Margin="3,0" Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}"
Foreground="{Binding Path=ConsistencyState, Converter={StaticResource ConsistencyStateToColorConverter}}"/>
<Image Opacity="0.75" HorizontalAlignment="Right" Margin="0,0"
Source="{Binding Path=TypeSelectionViewModel.MainViewModel.DragDropIcon}"
ToolTip="{x:Static res:TextLibrary.TEXT_DRAGCOPYCMT}">
<Image.Visibility>
<MultiBinding Converter="{StaticResource DragDropIconVisibilityConverter}">
<Binding Path="TypeSelectionViewModel.SelectedType" />
<Binding Path="." />
</MultiBinding>
</Image.Visibility>
</Image>
</DockPanel>-->
<!--</DataTemplate>-->
</Expander>
</DataTemplate>
</ListView.ItemTemplate>
</res:TypesListView>
</DockPanel>
Item Source for TypeListView is "Types", where "Types" is an observable collection of listviewItemViewModel. Now, I have Used an Expander to expand each ListViewItemViewModel, when user click on expand button it can have again an observable collection of listviewitemviewmodel.
Just a snippet of ListViewItemViewModel and what exactly is Types, Just a rough code:
class ListViewItemViewMOdel
{
public string Name
{
return "SomeString";
}
private ObservableCollection<ListViewItemViewModel> _variants;
public ObservableCollection<ListViewItemViewModel> Variants
{
get
{
return _variants;
}
}
}
TypeSelectionViewModel Class:
public class TypeSelectionViewModel : INotifyPropertyChanged
{
#region Fields
private readonly MainViewModel _mainViewModel;
private readonly ObservableCollection<ListViewItemViewModel> _types = new ObservableCollection<ListViewItemViewModel>();
private List<ListViewItemViewModel> _selectedTypes = new List<ListViewItemViewModel>();
private ListViewItemViewModel _selectedType;
#endregion (Fields)
public ListViewItemViewModel SelectedType
{
get { return _selectedType; }
set
{
if (_selectedType != value)
{
//ensure that only one Item can be selected (e.g. unset selection if a variant gets selected)
if (_selectedType != null && _selectedType.IsSelected)
_selectedType.IsSelected = false;
_selectedType = value;
if (_selectedType != null)
{
_mainViewModel.TypeStructureViewModel.RootTreeViewElement.Add(
new TreeViewItemViewModel(_selectedType.AutomationObjectBase, null, _mainViewModel));
if (_mainViewModel.TypeStructureViewModel.RootTreeViewElement.Count > 1)
_mainViewModel.TypeStructureViewModel.RootTreeViewElement.RemoveAt(0);
// select root element in TreeView and expand it
_mainViewModel.TypeStructureViewModel.RootTreeViewElement[0].IsExpanded = true;
//_mainViewModel.TypeStructureViewModel.ListSelected = new List<TreeViewItemViewModel>() { _mainViewModel.TypeStructureViewModel.RootTreeViewElement[0]};
_mainViewModel.TypeStructureViewModel.RootTreeViewElement[0].IsSelected = true;
}
else
{
// clear the treeview
_mainViewModel.TypeStructureViewModel.RootTreeViewElement.Clear();
//_mainViewModel.TypeStructureViewModel.ListSelected = new List<TreeViewItemViewModel>();
}
OnPropertyChanged("SelectedType");
}
}
}
public List<ListViewItemViewModel> SelectedTypes
{
get { return _selectedTypes; }
set { _selectedTypes = value; }
}
public ObservableCollection<ListViewItemViewModel> Types
{
get { return _types; }
}
public MainViewModel MainViewModel
{
get { return _mainViewModel; }
}
public ICollectionView TypeCollectionView
{
get
{
return CollectionViewSource.GetDefaultView(Types); // adcrst2 -> moved from xaml-codebehind
}
}
#endregion (Properties)
#region Ctor
internal TypeSelectionViewModel(MainViewModel mainViewModel)
{
_mainViewModel = mainViewModel;
// initialize commands...
ExpandCollapseAllTypesCmd = new ExpandCollapseAllTypesCmd(this);
TypeCollectionView.Filter = TypeFilter;
//TODO:
IsCommandHandling = false;
}
#endregion (Ctor)
#region Methods
public void AddType(AutomationObjType automationObjType)
{
AutomationObjectBase addedType;
if (automationObjType == AutomationObjType.CMT)
{
CMTCollectionObj cmtCollectionObj = AutomationObjectFactory.AddChild(CMTContainer.Instance, AutomationObjType.CMTCollectionObj) as CMTCollectionObj;
cmtCollectionObj.AddChild(AutomationObjType.CMTemplateObj);
addedType = cmtCollectionObj.MasterCMTemplate.CMBase;
}
else if (automationObjType == AutomationObjType.AggregatedCMTemplate)
{
CMTCollectionObj cmtCollectionObj = AutomationObjectFactory.AddChild(CMTContainer.Instance, AutomationObjType.CMTCollectionObj) as CMTCollectionObj;
addedType = cmtCollectionObj.AddChild(AutomationObjType.AggregatedCMTemplate);
}
else if (automationObjType == AutomationObjType.EMT)
{
var emAggregator = AutomationObjectFactory.AddChild(EMTContainer.Instance, AutomationObjType.EMTemplateObj) as EMTemplateObj;
addedType = emAggregator.EMBase;
}
else
{
AutomationObjectBase parent;
if (automationObjType == AutomationObjType.EMT)
parent = EMTContainer.Instance;
else if (automationObjType == AutomationObjType.Function)
parent = FunctionContainer.Instance;
else
parent = EnumerationTypes.Instance;
addedType = AutomationObjectFactory.AddChild(parent, automationObjType);
}
var newType = new ListViewItemViewModel(addedType, this);
// ApplyDefaultValuesForAllAttributes and for all auto created childs
newType.AutomationObjectBase.ApplyDefaultValuesForAllAttributesRecursive();
// only Workaround:
// ADBEMI15, Michael Berenz, 01.08.2013: implemented a Save() method for Framework. moved Ivans change to this method.
// Ivan P, F37771, 15.07.2013: Save EMT after create to avoid inconsistency when TypeConfigurator
// is closed without saving (Comment Michael B.: triggered by the implementation of the sequence chain. TODO: try to find better solution)
if (newType.AutomationObjectBase is EMT ||
newType.AutomationObjectBase is AggregatedCMTemplate ||
newType.AutomationObjectBase is CMBase)
{
newType.AutomationObjectBase.Parent.Save();
newType.AutomationObjectBase.Save();
}
Types.Add(newType);
SelectedType = newType;
}
#endregion
}//end TypesOverViewViewModel
Now, we have a control template implemented for expander control. In that control module toggle button visibility is binded to a converter. The purpose of converter is to show toggle button only when there is at least one child in each listviewitemviewModel.
Again just a rough code to show how converter is used to show the visibility of toggle button in an expander somewhere in ResouceDictionary.xaml file:
<local:ExpanderVisibilityConverter x:Key="ExpanderVisibilityConverter" />
<ControlTemplate x:Key="LazyExpanderTemplate" TargetType="Expander">
<Border Name="OuterExpanderBorder"
CornerRadius="3"
BorderThickness="{TemplateBinding Border.BorderThickness}"
BorderBrush="{TemplateBinding Border.BorderBrush}"
Background="{TemplateBinding Panel.Background}"
SnapsToDevicePixels="True">
<DockPanel>
<!--<Border Name="headerBorder" Margin="0,0,0,1" CornerRadius="2" BorderThickness="1"-->
<Border DockPanel.Dock="Top" CornerRadius="2" BorderThickness="1"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
SnapsToDevicePixels="True"
HorizontalAlignment="Stretch">
<Border DockPanel.Dock="Top" Name="InnerBorder"
CornerRadius="1"
BorderThickness="1">
<!--BorderBrush="{TemplateBinding BorderBrush}">-->
<Grid DockPanel.Dock="Top">
<Grid.RowDefinitions>
<RowDefinition MaxHeight="11"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Rectangle Name="UpperHighlight"
Visibility="Collapsed"
Fill="#75FFFFFF"/>
<DockPanel DockPanel.Dock="Top" Name="HeaderSite"
Grid.RowSpan="2" >
<ToggleButton
DockPanel.Dock="Right"
IsChecked="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=IsExpanded}"
Foreground="{TemplateBinding TextElement.Foreground}"
FontFamily="{TemplateBinding TextElement.FontFamily}"
FontSize="{TemplateBinding TextElement.FontSize}"
FontStretch="{TemplateBinding TextElement.FontStretch}"
FontStyle="{TemplateBinding TextElement.FontStyle}"
FontWeight="{TemplateBinding TextElement.FontWeight}"
HorizontalContentAlignment="{TemplateBinding Control.HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding Control.VerticalContentAlignment}"
Padding="{TemplateBinding Control.Padding}"
Visibility="{Binding Path=., Converter={StaticResource ExpanderVisibilityConverter}}"
MinWidth="0"
MinHeight="0"
Margin="1">
<ToggleButton.Style>
<Style TargetType="ToggleButton">
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Border Padding="{TemplateBinding Control.Padding}">
<Grid Background="Transparent" SnapsToDevicePixels="False">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="14" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Ellipse Stroke="#FFA9A9A9" Name="circle" Width="14" Height="14" HorizontalAlignment="Center" VerticalAlignment="Center" />
<Ellipse Name="shadow" Width="15" Height="15" HorizontalAlignment="Center" VerticalAlignment="Center" Visibility="Hidden" />
<!--TODO: Muss das sein-->
<Path Data="M1,1.5L4.5,5 8,1.5" Stroke="#FF666666" StrokeThickness="2" Name="arrow" HorizontalAlignment="Center" VerticalAlignment="Center" SnapsToDevicePixels="False" />
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="ToggleButton.IsChecked" Value="True" >
<Setter Property="Path.Data" TargetName="arrow">
<Setter.Value>
<StreamGeometry>M1,4.5L4.5,1 8,4.5</StreamGeometry>
</Setter.Value>
</Setter>
</Trigger>
<Trigger Property="UIElement.IsMouseOver" Value="True">
<Setter Property="Shape.Stroke" TargetName="circle">
<Setter.Value>
<SolidColorBrush>#FF666666</SolidColorBrush>
</Setter.Value>
</Setter>
<Setter Property="Shape.Stroke" TargetName="arrow">
<Setter.Value>
<SolidColorBrush>#FF222222</SolidColorBrush>
</Setter.Value>
</Setter>
<Setter Property="UIElement.Visibility" TargetName="shadow">
<Setter.Value>
<x:Static Member="Visibility.Visible" />
</Setter.Value>
</Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ToggleButton.Style>
<ToggleButton.FocusVisualStyle>
<Style TargetType="IFrameworkInputElement">
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate>
<Border>
<Rectangle Stroke="#FF000000" StrokeThickness="1" StrokeDashArray="1 2" Margin="0,0,0,0" SnapsToDevicePixels="True" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ToggleButton.FocusVisualStyle>
</ToggleButton>
<ContentPresenter
RecognizesAccessKey="True"
Content="{TemplateBinding HeaderedContentControl.Header}"
ContentTemplate="{TemplateBinding HeaderedContentControl.HeaderTemplate}"
ContentStringFormat="{TemplateBinding HeaderedContentControl.HeaderStringFormat}"
Margin="4,0,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
SnapsToDevicePixels="True"/>
</DockPanel>
</Grid>
</Border>
</Border>
<ContentPresenter Content="{TemplateBinding ContentControl.Content}"
ContentTemplate="{TemplateBinding ContentControl.ContentTemplate}"
ContentStringFormat="{TemplateBinding ContentControl.ContentStringFormat}"
Name="ExpandSite"
Margin="{TemplateBinding Control.Padding}"
HorizontalAlignment="{TemplateBinding Control.HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding Control.VerticalContentAlignment}"
Visibility="Collapsed"
Focusable="False"
DockPanel.Dock="Bottom" />
</DockPanel>
</Border>
</ControlTemplate>
ExpanderVisibilityConverter is the class which implements Convert method of IValueConverter interface.In Convert method, each listViewItemVIewModel is checked for its child element, if child, return visibility.visible else visibility.collapsed
The problem is, if I start filling "Types" at run time after running the application and I added first listviewItemVIewModel in "Types", ExpanderVisibilityConverter.Convert is invoked where I checked if listviewItemViewModel has child, in this case it is not having any child and thus the toggle button is not shown. Now I added a child to listviewItemViewModel, now this time also togglebutton is not shown as the ExpanderVisibilityConverter.Convert method is not invoked for child element.
Now I want the toggle button visible right at the moment when I add child to listViewItemViewModel. Please help me to achieve this.
How I have implemented my converter.
public class ExpanderVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var tvItem = value as ListViewItemViewModel;
if (tvItem != null && tvItem.Variants.Count > 0)
{
return Visibility.Visible;
}
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return Binding.DoNothing;
}
}
I have added image of my actual UI part:
Please have a look on below image:
You have to some changes to your ViewModel
. Right now your Variants
property is not propagating changes to the bounded item (ToggleButton
). So,
we need to notify binding mechanism when our ObservableCollection
changes, we will do this using CollectionChanged
event.
And finally we will change the ToggleButton binding
to {Binding Path=Variants, Converter={StaticResource ExpanderVisibilityConverter}}
class ListViewItemViewMOdel:INotifyPropertyChanged
{
public string Name
{
return "SomeString";
}
private ObservableCollection<ListViewItemViewModel> _variants;
public ObservableCollection<ListViewItemViewModel> Variants
{
get
{
return _variants;
}
}
public ListViewItemViewMOdel()
{
_variants.CollectionChanged+=_variants_CollectionChanged;
}
void _variants_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
OnPropertyChanged("Variants");
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string prop)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(prop));
}
}
Run the above code and tell what happens ?