memory card game wpf problem - wpf

I have a memorey card game where my binding is to
public ObservableCollection<List<memoryCard>> MyCollection { get; set; }//holding the array
where is initlized like this :
for (int i = 0; i < Size; i++)
{
List<memoryCard> list = new List<memoryCard>();
for (int j = 0; j < Size; j++)
{
var card = new memoryCard(createRandomBrush(array, j));
card.IsChecked = false;
list.Add(card);
card.PropertyChanged += (a, b) => Verify(b.PropertyName);
}
MyCollection.Add(list);
}
**EDIT its now working except to the one EDIT in the middle who do me an hard time **
private void Verify(string propName)
{
if (propName != "IsChecked2")
{
return;
}
// List<memoryCard> selected = new List<memoryCard>();
foreach (List<memoryCard> observerListItem in MyCollection)
foreach (memoryCard mycard in observerListItem)
if (mycard.IsChecked == true)
selected.Add(mycard);
if (selected.Count == 2)
{
if ((selected.First().buttonColor == selected.Last().buttonColor) &&
(!selected.First().Equals(selected.Last() ) ) )
{
if (firstClick)
{
MessageBox.Show("Good.", "Result", MessageBoxButton.OK, MessageBoxImage.Information);
firstClick = !firstClick;
selected.ForEach(cell => cell.buttonColor = Brushes.White);
//here the biding color of the toggle item should chang and it doesnt can someone explain me how to do it right ?ite
}
}
}
else
{
if (firstClick)
{
MessageBox.Show("Bad.", "Result", MessageBoxButton.OK, MessageBoxImage.Error);
firstClick = !firstClick;
}
}
selected.First().IsChecked = selected.Last().IsChecked = false;
selected.Clear();
firstClick = true;
}
if (selected.Count!=0)selected.First().IsChecked = false;
}
and my memoreycard class is :
public class memoryCard : INotifyPropertyChanged
{
#region c'tor
public memoryCard(Brush _buttonColor)
{
buttonColor=_buttonColor;
}
#endregion
private bool ?_isChecked = false;
public bool ?IsChecked
{
get
{
return _isChecked;
}
set
{
if (_isChecked != value)
{
_isChecked = value;
//OnPropertyChanged("IsChecked");
OnPropertyChanged("IsChecked2");
}
}
}
#region colorofbutton
public Brush buttonColor;
public Brush ButtonColor
{
get
{
return buttonColor;
}
set
{
buttonColor = value;
}
}
#endregion
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
}
I try to achieve the goal of a memory card game
check if there are two card who were IsCheck/ed property value = true and check there color if there's color is equal make this toggle button in a IsCheck/ed property null
but the prior algoritam doesnt work!!
this is an example fo what i am trying to achive by the end atoggle button who's changed according to the game
EDIT
how can I do it using the triggers when all the logic above is working ng ?...
<ControlTemplate x:Key="ButtonControlTemplate1" TargetType="{x:Type ToggleButton}">
<Grid>
<Ellipse Name="elipse1" Height="65" Width="79" Fill="{Binding Path=ButtonColor}" Visibility="Collapsed"></Ellipse>
<Ellipse Name="elipse2" Height="65" Width="79" Fill="Black" ></Ellipse>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="elipse1" Property="Visibility" Value="Visible"/>
<Setter TargetName="elipse2" Property="Visibility" Value="Collapsed"/>
</Trigger>
<Trigger Property="IsCheck" Value="null">
<Setter TargetName="elipse1" Property="Visibility" Value="Collapsed"/>
<Setter TargetName="elipse2" Property="Visibility" Value="Collapsed"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
** EDIT**
so to summrize i have two questiones about my code
i changed the card color to white in verfy why doesnt the toggle button associted to him changes his color as well?
how can i trugger a toggle button to be allways in one color? when a card gets new color ,White i want him to change his color to ehite how can i achieve it ?
thank you very much.
EDIT2
my only problem is thatbindingto the color and changing the color like this :
selected.ForEach(cell => cell.buttonColor = Brushes.White);
doesnt make the ui notice it , even thought the card property changed and call
OnPropertyChanged("ButtonColor");
the UI doesnt change it
EDIT3
now i want a triger that whent i am on it will give aany toggle button who have white color the white color
<ControlTemplate x:Key="ButtonControlTemplate1" TargetType="{x:Type ToggleButton}">
<Grid>
<Ellipse Name="elipse1" Height="65" Width="79" Fill="{Binding Path=ButtonColor}" Visibility="Collapsed"></Ellipse>
<Ellipse Name="elipse2" Height="65" Width="79" Fill="Black" ></Ellipse>
<Ellipse Name="elipse3" Height="65" Width="79" Fill="Black" Visibility="Collapsed" ></Ellipse>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="elipse1" Property="Visibility" Value="Visible"/>
<Setter TargetName="elipse2" Property="Visibility" Value="Collapsed"/>
</Trigger>
<Trigger Property="IsCheck" Value="null">
<Setter TargetName="elipse1" Property="Visibility" Value="Collapsed"/>
<Setter TargetName="elipse2" Property="Visibility" Value="Collapsed"/>
<Setter TargetName="elipse3" Property="Visibility" Value="Visible"/>
</Trigger>

When you set your ButtonColor, call PropertyChanged. So:
#region colorofbutton
private Brush buttonColor; // Use the property accessors instead
public Brush ButtonColor
{
get
{
return buttonColor;
}
set
{
buttonColor = value;
OnPropertyChanged("ButtonColor");
}
}
#endregion

Related

How to switch Control by DataTrigger in the xaml?

There is a window consist of the two control.
one is TreeView, the other is ListBox.
The code is as shown below.
<Grid Grid.Row="1">
<TreeView x:Name="treeView" BorderThickness="0" Visibility="Visible"
ItemsSource="{Binding TotalCPUs}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectedItemChanged">
<mvvm:EventToCommand Command="{Binding CPUSelectedCommand}"
PassEventArgsToCommand="True"
EventArgsConverter="{localConverters:SelectedItemConverter}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</TreeView>
<ListBox x:Name="listBox" BorderThickness="0" Visibility="Collapsed"/>
</Grid>
Now I would like to change the value of the Visibility property of the Control whenever changes property of the ViewModel. (FilterMode True = ListBox Visible, FilterMode False = TreeView = Visible)
To this, I modified my XAML code as below.
<Grid Grid.Row="1">
<TreeView x:Name="treeView" BorderThickness="0" Visibility="Visible"
ItemsSource="{Binding TotalCPUs}">
<TreeView.Style>
<Style TargetType="{x:Type TreeView}">
<Style.Triggers>
<DataTrigger Binding="{Binding FilterMode}" Value="true">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TreeView.Style>
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectedItemChanged">
<mvvm:EventToCommand Command="{Binding CPUSelectedCommand}"
PassEventArgsToCommand="True"
EventArgsConverter="{localConverters:SelectedItemConverter}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</TreeView>
<ListBox x:Name="listBox" BorderThickness="0" Visibility="Collapsed">
<ListBox.Style>
<Style TargetType="{x:Type ListBox}">
<Style.Triggers>
<DataTrigger Binding="{Binding FilterMode}" Value="true">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ListBox.Style>
</ListBox>
</Grid>
Below is the ViewModel code.
public class NewProjectViewModel : DialogViewModel
{
private Generator projectGenerator = new Generator();
public ObservableCollection<ClassHierarchyData> TotalCPUs { get; private set; } = new ObservableCollection<ClassHierarchyData>();
public ObservableCollection<DetailType> FilterCPUs { get; private set; } = new ObservableCollection<DetailType>();
private bool filterMode;
public bool FilterMode
{
get => filterMode;
set
{
if (this.filterMode == value) return;
this.filterMode = value;
this.RaisePropertyChanged("FilterMode");
}
}
private string cpuSearch;
public string CPUSearch
{
get => this.cpuSearch;
set
{
if (this.cpuSearch == value) return;
this.cpuSearch = value;
this.FilterCPUs.Add(new DetailType(typeof(Target), "abc"));
}
}
private Type selectedTerminalItem;
public Type SelectedTerminalItem
{
get => this.selectedTerminalItem;
private set
{
if (this.selectedTerminalItem == value) return;
this.selectedTerminalItem = value;
this.RaisePropertyChanged("SelectedTerminalItem");
}
}
private Type selectedItem;
public Type SelectedItem
{
get => selectedItem;
set
{
if (this.selectedItem == value) return;
this.selectedItem = value;
this.RaisePropertyChanged("SelectedItem");
CreateCommand.RaiseCanExecuteChanged();
}
}
private string solutionName = string.Empty;
public string SolutionName
{
get => this.solutionName;
set
{
if (this.solutionName == value) return;
this.solutionName = value;
this.RaisePropertyChanged("SolutionName");
this.RaisePropertyChanged("SolutionFullPath");
CreateCommand.RaiseCanExecuteChanged();
}
}
private string solutionPath = string.Empty;
public string SolutionPath
{
get => this.solutionPath;
set
{
if (this.solutionPath == value) return;
this.solutionPath = value;
if(this.SolutionPath.Length > 0)
{
if (this.solutionPath.Last() != '\\')
this.solutionPath += "\\";
}
this.RaisePropertyChanged("SolutionPath");
this.RaisePropertyChanged("SolutionFullPath");
CreateCommand.RaiseCanExecuteChanged();
}
}
public bool CreateSolutionFolder { get; set; }
public string SolutionFullPath { get => this.SolutionPath + this.solutionName; }
private RelayCommand searchCommand;
public RelayCommand SearchCommand
{
get
{
if (this.searchCommand == null) this.searchCommand = new RelayCommand(this.OnSearch);
return this.searchCommand;
}
}
private void OnSearch()
{
CommonOpenFileDialog selectFolderDialog = new CommonOpenFileDialog();
selectFolderDialog.InitialDirectory = "C:\\Users";
selectFolderDialog.IsFolderPicker = true;
if (selectFolderDialog.ShowDialog() == CommonFileDialogResult.Ok)
{
this.SolutionPath = selectFolderDialog.FileName + "\\";
}
}
private RelayCommand<Action> _createCommand;
public RelayCommand<Action> CreateCommand
{
get
{
if (this._createCommand == null)
this._createCommand = new RelayCommand<Action>(this.OnCreate, this.CanExecuteCreate);
return this._createCommand;
}
}
private void OnCreate(Action action)
{
projectGenerator.GenerateSolution(this.SolutionPath, this.SolutionName, this.CreateSolutionFolder);
action?.Invoke();
}
private bool CanExecuteCreate(Action action)
{
if (this.SelectedTerminalItem == null) return false;
if (string.IsNullOrEmpty(this.solutionPath)) return false;
if (string.IsNullOrEmpty(this.solutionName)) return false;
return true;
}
private RelayCommand<ClassHierarchyData> cpuSelectedCommand;
public RelayCommand<ClassHierarchyData> CPUSelectedCommand
{
get
{
if (this.cpuSelectedCommand == null)
this.cpuSelectedCommand = new RelayCommand<ClassHierarchyData>(OnCPUSelected);
return this.cpuSelectedCommand;
}
}
private void OnCPUSelected(ClassHierarchyData selected)
{
this.SelectedItem = selected.Data;
this.SelectedTerminalItem = (selected.Items.Count == 0) ? selected.Data : null;
}
private RelayCommand<string> navigateCommand;
public RelayCommand<string> NavigateCommand
{
get
{
if (this.navigateCommand == null)
this.navigateCommand = new RelayCommand<string>((uri) =>
{
Process.Start(new ProcessStartInfo(uri));
});
return navigateCommand;
}
}
public NewProjectViewModel()
{
ClassHierarchyGenerator classHierarchyGenerator = new ClassHierarchyGenerator();
this.TotalCPUs.Add(classHierarchyGenerator.ToHierarchyData(typeof(Target)));
this.FilterCPUs.CollectionChanged += FilterCPUs_CollectionChanged;
}
private void FilterCPUs_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
this.FilterMode = (this.FilterCPUs.Count > 0) ? true : false;
}
}
public class DetailType
{
public Type Type { get; }
public string Path { get; }
public DetailType(Type type, string path)
{
Type = type;
Path = path;
}
}
If user input data to the TextBox for filtering then the value of the CPUSearch is changed.
If the value of the CPUSearch is changed then be added test value into the FilterCPUs. (Note CPUSearch property)
when being added value into the FilterCPUs, FilterCPUs_CollectionChanged is called and the value of the FilterMode is changed.
But the above code does not work although be changed the value of the FilterMode. (works well except for functionality related to FilterMode)
Why doesn't the Control switch?
Thanks for reading.
I solved this problem by referring WPF Showing / Hiding a control with triggers
My XAML code updated like this and it works well.
Thanks for your interest.
<Grid Grid.Row="1">
<TreeView x:Name="treeView" BorderThickness="0"
ItemsSource="{Binding TotalCPUs}">
<TreeView.Style>
<Style TargetType="{x:Type TreeView}">
<Style.Triggers>
<DataTrigger Binding="{Binding FilterMode}" Value="true">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
<DataTrigger Binding="{Binding FilterMode}" Value="false">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TreeView.Style>
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectedItemChanged">
<mvvm:EventToCommand Command="{Binding CPUSelectedCommand}"
PassEventArgsToCommand="True"
EventArgsConverter="{localConverters:SelectedItemConverter}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</TreeView>
<ListBox x:Name="listBox" BorderThickness="0"
ItemsSource="{Binding FilterCPUs}"
SelectedItem="{Binding SelectedItem}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock FontSize="8" Text="{Binding Path=Path}" Margin="0 0 0 3"/>
<TextBlock Text="{Binding Type.Name}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.Style>
<Style TargetType="{x:Type ListBox}">
<Setter Property="Background" Value="{DynamicResource CommonEnableBackgroundBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource CommonEnableBorderBrush}"/>
<Setter Property="Foreground" Value="{DynamicResource CommonEnableTextBrush}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding FilterMode}" Value="true">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
<DataTrigger Binding="{Binding FilterMode}" Value="false">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ListBox.Style>
</ListBox>
</Grid>

Listbox item trigger colour change

I am trying to change the colour of the items in a listbox based on a trigger using MVVM
<Border Grid.Row="1" Width="300" Margin="0,0,20,0" BorderThickness="1,2,1,1" CornerRadius="5" BorderBrush="#FF999393" Background="#FFE9EDF1" >
<ListBox ItemsSource="{Binding LogMessageList, UpdateSourceTrigger=PropertyChanged}" Background="{x:Null}" Margin="3" BorderBrush="{x:Null}" FontSize="13.333" >
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Foreground" Value="#FF403E3E" />
<Style.Triggers>
<DataTrigger Binding="{Binding FatalError, UpdateSourceTrigger=PropertyChanged}" Value="Fatal">
<Setter Property="Foreground" Value="Firebrick" />
</DataTrigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
I'm setting the property change correctly but nothing seems to change.
Thanks
EDIT:
Still stuck. Trying
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Foreground" Value="#FF403E3E" />
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}},Path=DataContext.FatalError, UpdateSourceTrigger=PropertyChanged}" Value="Fatal">
<Setter Property="Foreground" Value="Firebrick" />
</DataTrigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
Tried your scenario. Please refer the below code example:
View Model
public class Vm : INotifyPropertyChanged
{
public ObservableCollection<VmUser> VmUsers { get; set; }
private string errorType;
public string ErrorType
{
get { return errorType; }
set
{
errorType = value;
Raise("ErrorType");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void Raise(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
public class VmUser
{
public string Name { get; set; }
public int Age { get; set; }
}
Set the DataContext of window in constructor :
public MainWindow()
{
Vm = new Vm
{
VmUsers = new ObservableCollection<VmUser>
{
new VmUser { Name = "Gil", Age = 1 },
new VmUser { Name = "Dan", Age = 2 },
new VmUser { Name = "John", Age = 3 },
},
ErrorType = "Fatal"
};
InitializeComponent();
DataContext = TheVm;
}
Defined ListBox in XAML as:
<ListBox Grid.Row="2" ItemsSource="{Binding VmUsers}" DisplayMemberPath="Name" SelectedValuePath="Age">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Foreground" Value="#FF403E3E" />
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}},Path=DataContext.ErrorType, UpdateSourceTrigger=PropertyChanged}" Value="Fatal">
<Setter Property="Foreground" Value="Red" />
</DataTrigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
To test the scenario I have wrote a small test to toggle the ErrorType on click on button
private void Button_Click(object sender, RoutedEventArgs e)
{
var vm = this.DataContext as Vm;
if (vm != null)
{
if (vm.ErrorType == "Fatal")
{
vm.ErrorType = "Non Fatal";
}
else
{
vm.ErrorType = "Fatal";
}
}
}
With above example foreground color changes based on ErrorType.

WPF: setting an attached property of custom control via style setter in ItemsControl

I have ItemsControl in which the ItemsPanel is a Canvas. The data template is a Canvas wrapped in a Border and it has additional Rectangle in it. This is the ItemsControl XAML:
<ItemsControl x:Name="Items" ItemsSource="{Binding TileResources, ElementName=ResourceWindow}" >
<ItemsControl.ItemsPanel >
<ItemsPanelTemplate>
<Canvas Background="SkyBlue" Width="500" Height="500" IsItemsHost="True" Loaded="Canvas_Loaded_1" MouseDown="Canvas_MouseDown" MouseUp="Canvas_MouseUp" MouseMove="Canvas_MouseMove"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:ResourceBorderControl x:Name="ResRect" BorderThickness="2"
MouseDown="selectionBox_MouseDown" MouseUp="selectionBox_MouseUp" >
<Canvas Width="{Binding Width, ElementName=ResRect}" Height="{Binding Height, ElementName=ResRect}" Background="White" Opacity="0.1">
<Rectangle Stroke="Red" StrokeThickness="2"
Canvas.Left="{Binding CollisionX, ElementName=ResRect}"
Canvas.Top="{Binding CollisionY, ElementName=ResRect}"
Width="{Binding CollisionWidth, ElementName=ResRect}"
Height="{Binding CollisionHeight, ElementName=ResRect}"
/>
</Canvas>
</local:ResourceBorderControl>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsSelected}" Value="true">
<Setter Property="BorderBrush" Value="Purple" TargetName="ResRect"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsSelected}" Value="false">
<Setter Property="BorderBrush" Value="SeaGreen" TargetName="ResRect"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Canvas.Left" Value="{Binding X}" />
<Setter Property="Canvas.Top" Value="{Binding Y}" />
<Setter Property="FrameworkElement.Width" Value="{Binding Width}" />
<Setter Property="FrameworkElement.Height" Value="{Binding Height}" />
<Setter Property="local:ResourceBorderControl.CollisionX" Value="{Binding CollideX}" />
<Setter Property="local:ResourceBorderControl.CollisionY" Value="{Binding CollideY}" />
<Setter Property="local:ResourceBorderControl.CollisionWidth" Value="{Binding CollideWidth}" />
<Setter Property="local:ResourceBorderControl.CollisionHeight" Value="{Binding CollideHeight}" />
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
As you can see, I am using custom Border control - ResourceBorderControl. It has 4 attached properties which describe the inner rectangle:
CollisionX
CollisionY
CollisionWidth
CollisionHeight
It works fine if I just hardcode the attached properties (CollisionX and so on) in XAML - the Rectangle in Canvas is rendered as I want it to be.
I want it to be set using the style (or any other way if it's possible) because I need it to be filled with values from rendered object from ItemsSource.
And it just doesn't work. I can't even debug it in WPF tree visualizer - when I try to look at ResourceBorderControl object I can't see the Collision values. When I run the program, I can see rendering of Border with Canvas (I can see the opacity) but there isn't anything in it. I suppose these values get lost after being set by Style Setters.
So my question is - am I doing anything wrong here? Or it's just WPF that does not allow setting custom attached values using styles and I should use another approach? If latter, how else can I do it?
Edit:
This is the code of class which instances are in ObservableCollection that is binded to ItemsSource
public class ResourceModelBase : IResourceModel
{
private String _name, _defaultLayer;
private double _x, _y, _width, _height, _collideX, _collideY, _collideWidth, _collideHeight;
private bool _isSelected;
public ResourceModelBase(double x, double y, double width, double height)
{
X = x;
Y = y;
Width = width;
Height = height;
CollideX = 1;
CollideY = 1;
CollideWidth = 100;
CollideHeight = 100;
IsSelected = false;
Name = "RES_" + width.ToString() + height.ToString();
}
public void Select()
{
IsSelected = true;
}
public void Deselect()
{
IsSelected = false;
}
public new String ToString()
{
return Name;
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public String Name
{
get
{
return _name;
}
set
{
if (value != _name)
{
_name = value;
NotifyPropertyChanged();
}
}
}
public String DefaultLayer
{
get
{
return _defaultLayer;
}
set
{
if (value != _defaultLayer)
{
_defaultLayer = value;
NotifyPropertyChanged();
}
}
}
public double X
{
get
{
return _x;
}
set
{
if (value != _x)
{
_x = value;
NotifyPropertyChanged();
}
}
}
public double Y
{
get
{
return _y;
}
set
{
if (value != _y)
{
_y = value;
NotifyPropertyChanged();
}
}
}
public double Width
{
get
{
return _width;
}
set
{
if (value != _width)
{
_width = value;
NotifyPropertyChanged();
}
}
}
public double Height
{
get
{
return _height;
}
set
{
if (value != _height)
{
_height = value;
NotifyPropertyChanged();
}
}
}
public bool IsSelected
{
get
{
return _isSelected;
}
set
{
if (value != _isSelected)
{
_isSelected = value;
NotifyPropertyChanged();
}
}
}
public double CollideX
{
get
{
return _collideX;
}
set
{
if (value != _collideX)
{
_collideX = value;
NotifyPropertyChanged();
}
}
}
public double CollideY
{
get
{
return _collideY;
}
set
{
if (value != _collideY)
{
_collideY = value;
NotifyPropertyChanged();
}
}
}
public double CollideWidth
{
get
{
return _collideWidth;
}
set
{
if (value != _collideWidth)
{
_collideWidth = value;
NotifyPropertyChanged();
}
}
}
public double CollideHeight
{
get
{
return _collideHeight;
}
set
{
if (value != _collideHeight)
{
_collideHeight = value;
NotifyPropertyChanged();
}
}
}
}
As you can see- mostly public properties, plus implementation of INotifyPropertyChanged interface.
Attached property definition:
public static readonly DependencyProperty CollisionX = DependencyProperty.RegisterAttached(
"CollisionX",
typeof(double),
typeof(ResourceBorderControl),
new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender)
);
public static void SetCollisionX(UIElement element, object value)
{
element.SetValue(CollisionX, (double)value);
}
public static double GetCollisionX(UIElement element)
{
return (double)element.GetValue(CollisionX);
}
Your attached property looks OK but I think you're misunderstanding the relationship of the ItemTemplate and ItemContainerStyle. It looks like you're trying to set the properties that you're binding in the ItemContainerStyle and have them surface on your ResourceBorderControl inside the DataTemplate which will be injected into the item container. What you're actually getting with what you have now is a visual tree like this:
ItemsControl
ItemsPresenter
Canvas
ContentPresenter for item 1 (ResourceBorderControl.CollisionX = bound value, etc)
ResourceBorderControl
Canvas
...
ContentPresenter for item 2 (ResourceBorderControl.CollisionX = bound value, etc)
ResourceBorderControl
Canvas
...
There are two ways you can handle this depending on what your ultimate goal is. If you just want to get the properties to show up down on the ResourceBorderControl, you can take advantage of property inheritance by changing your DP definition to include an extra Inherits flag:
public static readonly DependencyProperty CollisionX = DependencyProperty.RegisterAttached(
"CollisionX",
typeof(double),
typeof(ResourceBorderControl),
new FrameworkPropertyMetadata(0.0,
FrameworkPropertyMetadataOptions.Inherits | FrameworkPropertyMetadataOptions.AffectsRender)
);
This will cause the property to be set on each child below the ContentPresenter ItemContainer unless overridden or cleared.
Your other choice would be to derive a custom ItemsControl which uses ResourceBorderControl as its container. This involves just overriding a few methods, I think GetContainerForItemOverride and IsItemItsOwnContainerOverride are the minimum. This gives you the benefit of your custom control being a direct child of the ItemsPanel's Canvas, which can open up lots of different layout options.
I figured out how to make a workaround, but this is ugly hack, so if anyone is able to point me in the right direction I would appreciate that :). I added DataTriggers, so now my DataTemplate.Triggers XAML node looks like this:
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsSelected}" Value="true">
<Setter Property="BorderBrush" Value="Purple" TargetName="ResRect"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsSelected}" Value="false">
<Setter Property="BorderBrush" Value="SeaGreen" TargetName="ResRect"/>
</DataTrigger>
<!-- very very ugly ugly hack :(-->
<DataTrigger Binding="{Binding IsSelected}" Value="true">
<Setter Property="Canvas.Left" Value="{Binding CollideX}" TargetName="CollideRect"/>
<Setter Property="Canvas.Top" Value="{Binding CollideY}" TargetName="CollideRect"/>
<Setter Property="FrameworkElement.Width" Value="{Binding CollideWidth}" TargetName="CollideRect"/>
<Setter Property="FrameworkElement.Height" Value="{Binding CollideHeight}" TargetName="CollideRect"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsSelected}" Value="false">
<Setter Property="Canvas.Left" Value="{Binding CollideX}" TargetName="CollideRect"/>
<Setter Property="Canvas.Top" Value="{Binding CollideY}" TargetName="CollideRect"/>
<Setter Property="FrameworkElement.Width" Value="{Binding CollideWidth}" TargetName="CollideRect"/>
<Setter Property="FrameworkElement.Height" Value="{Binding CollideHeight}" TargetName="CollideRect"/>
</DataTrigger>
</DataTemplate.Triggers>
I am not happy about it, but at least it works.

TabControl with Add New Tab Button (+)

What is the proper way of adding a '+' button tab at the end of all the tab items in the tab strip of a tab control in WPF?
It should work correctly with multiple tab header rows.
It should be at the end of all tab items
Tab cycling should work correctly (Alt + Tab), that is, the + tab should be skipped.
I shouldn't have to modify the source collection I am binding to. That is, the control should be reusable.
The solution should work with MVVM
To be more precise, the button should appear exactly as an additional last tab and not as a separate button somewhere on the right of all tab strip rows.
I am just looking for the general approach to doing this.
Google throws many examples, but if you dig a little deep none of them satisfy all the above five points.
An almost complete solution using IEditableCollectionView:
ObservableCollection<ItemVM> _items;
public ObservableCollection<ItemVM> Items
{
get
{
if (_items == null)
{
_items = new ObservableCollection<ItemVM>();
var itemsView = (IEditableCollectionView)CollectionViewSource.GetDefaultView(_items);
itemsView.NewItemPlaceholderPosition = NewItemPlaceholderPosition.AtEnd;
}
return _items;
}
}
private DelegateCommand<object> _newCommand;
public DelegateCommand<object> NewCommand
{
get
{
if (_newCommand == null)
{
_newCommand = new DelegateCommand<object>(New_Execute);
}
return _newCommand;
}
}
private void New_Execute(object parameter)
{
Items.Add(new ItemVM());
}
<DataTemplate x:Key="newTabButtonContentTemplate">
<Grid/>
</DataTemplate>
<DataTemplate x:Key="newTabButtonHeaderTemplate">
<Button Content="+"
Command="{Binding ElementName=parentUserControl, Path=DataContext.NewCommand}"/>
</DataTemplate>
<DataTemplate x:Key="itemContentTemplate">
<Grid/>
</DataTemplate>
<DataTemplate x:Key="itemHeaderTemplate">
<TextBlock Text="TabItem_test"/>
</DataTemplate>
<vw:TemplateSelector x:Key="headerTemplateSelector"
NewButtonTemplate="{StaticResource newTabButtonHeaderTemplate}"
ItemTemplate="{StaticResource itemHeaderTemplate}"/>
<vw:TemplateSelector x:Key="contentTemplateSelector"
NewButtonTemplate="{StaticResource newTabButtonContentTemplate}"
ItemTemplate="{StaticResource itemContentTemplate}"/>
<TabControl ItemsSource="{Binding Items}"
ItemTemplateSelector="{StaticResource headerTemplateSelector}"
ContentTemplateSelector="{StaticResource contentTemplateSelector}"/>
public class TemplateSelector : DataTemplateSelector
{
public DataTemplate ItemTemplate { get; set; }
public DataTemplate NewButtonTemplate { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
if (item == CollectionView.NewItemPlaceholder)
{
return NewButtonTemplate;
}
else
{
return ItemTemplate;
}
}
}
Enter code here
It's almost complete, because the tab cycle doesn't skip the '+' tab, and will show empty content (which is not exactly great, but I can live with it until a better solution come around...).
Existing answers were too complex for me and I am lazy. So, I tried to implement a very simple idea.
Always add [+] tab to the last.
When the last tab is selected, make it as a new tab, and add another last tab.
The idea was simple, but the damn WPF is verbose, so the code became a little bit long. But it probably is very simple to understand... because even I did.
Code behind.
public partial class MainWindow : Window
{
int TabIndex = 1;
ObservableCollection<TabVM> Tabs = new ObservableCollection<TabVM>();
public MainWindow()
{
InitializeComponent();
var tab1 = new TabVM()
{
Header = $"Tab {TabIndex}",
Content = new ContentVM("First tab", 1)
};
Tabs.Add(tab1);
AddNewPlusButton();
MyTabControl.ItemsSource = Tabs;
MyTabControl.SelectionChanged += MyTabControl_SelectionChanged;
}
private void MyTabControl_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if(e.Source is TabControl)
{
var pos = MyTabControl.SelectedIndex;
if (pos!=0 && pos == Tabs.Count-1) //last tab
{
var tab = Tabs.Last();
ConvertPlusToNewTab(tab);
AddNewPlusButton();
}
}
}
void ConvertPlusToNewTab(TabVM tab)
{
//Do things to make it a new tab.
TabIndex++;
tab.Header = $"Tab {TabIndex}";
tab.IsPlaceholder = false;
tab.Content = new ContentVM("Tab content", TabIndex);
}
void AddNewPlusButton()
{
var plusTab = new TabVM()
{
Header = "+",
IsPlaceholder = true
};
Tabs.Add(plusTab);
}
class TabVM:INotifyPropertyChanged
{
string _Header;
public string Header
{
get => _Header;
set
{
_Header = value;
OnPropertyChanged();
}
}
bool _IsPlaceholder = false;
public bool IsPlaceholder
{
get => _IsPlaceholder;
set
{
_IsPlaceholder = value;
OnPropertyChanged();
}
}
ContentVM _Content = null;
public ContentVM Content
{
get => _Content;
set
{
_Content = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
void OnPropertyChanged([CallerMemberName] string property = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}
}
class ContentVM
{
public ContentVM(string name, int index)
{
Name = name;
Index = index;
}
public string Name { get; set; }
public int Index { get; set; }
}
private void OnTabCloseClick(object sender, RoutedEventArgs e)
{
var tab = (sender as Button).DataContext as TabVM;
if (Tabs.Count>2)
{
var index = Tabs.IndexOf(tab);
if(index==Tabs.Count-2)//last tab before [+]
{
MyTabControl.SelectedIndex--;
}
Tabs.RemoveAt(index);
}
}
}
XAML
<TabControl Name="MyTabControl">
<TabControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Header, Mode=OneWay}" />
<Button Click="OnTabCloseClick" Width="20" Padding="0" Margin="8 0 0 0" Content="X">
<Button.Style>
<Style TargetType="Button" x:Name="CloseButtonStyle">
<Setter Property="Visibility" Value="Visible"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsPlaceholder}" Value="True">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</StackPanel>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<ContentControl>
<ContentControl.Resources>
<ContentControl x:Key="TabContentTemplate">
<StackPanel DataContext="{Binding Content}" Orientation="Vertical">
<TextBlock Text="{Binding Path=Name}"/>
<TextBlock Text="{Binding Path=Index}"/>
</StackPanel>
</ContentControl>
</ContentControl.Resources>
<ContentControl.Style>
<Style TargetType="ContentControl">
<Style.Triggers>
<DataTrigger Binding="{Binding IsPlaceholder}" Value="True">
<Setter Property="Content"
Value="{x:Null}"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsPlaceholder}" Value="False">
<Setter Property="Content"
Value="{StaticResource TabContentTemplate}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
I used a modification of the tab control template and binding to the AddNewItemCommand command in my view model.
XAML:
<TabControl x:Class="MyNamespace.MyTabView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
ItemsSource="{Binding MyItemSource}"
SelectedIndex="{Binding LastSelectedIndex}"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Control.Template>
<ControlTemplate TargetType="{x:Type TabControl}">
<Grid ClipToBounds="true"
SnapsToDevicePixels="true"
KeyboardNavigation.TabNavigation="Local">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="ColumnDefinition0" />
<ColumnDefinition x:Name="ColumnDefinition1"
Width="0" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition x:Name="RowDefinition0"
Height="Auto" />
<RowDefinition x:Name="RowDefinition1"
Height="*" />
</Grid.RowDefinitions>
<StackPanel Grid.Column="0"
Grid.Row="0"
Orientation="Horizontal"
x:Name="HeaderPanel">
<TabPanel x:Name="_HeaderPanel"
IsItemsHost="true"
Margin="2,2,2,0"
KeyboardNavigation.TabIndex="1"
Panel.ZIndex="1" />
<Button Content="+"
Command="{Binding AddNewItemCommand}" />
</StackPanel>
<Border x:Name="ContentPanel"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
Grid.Column="0"
KeyboardNavigation.DirectionalNavigation="Contained"
Grid.Row="1"
KeyboardNavigation.TabIndex="2"
KeyboardNavigation.TabNavigation="Local">
<ContentPresenter x:Name="PART_SelectedContentHost"
ContentSource="SelectedContent"
Margin="{TemplateBinding Padding}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</Border>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="TabStripPlacement"
Value="Bottom">
<Setter Property="Grid.Row"
TargetName="HeaderPanel"
Value="1" />
<Setter Property="Grid.Row"
TargetName="ContentPanel"
Value="0" />
<Setter Property="Height"
TargetName="RowDefinition0"
Value="*" />
<Setter Property="Height"
TargetName="RowDefinition1"
Value="Auto" />
<Setter Property="Margin"
TargetName="HeaderPanel"
Value="2,0,2,2" />
</Trigger>
<Trigger Property="TabStripPlacement"
Value="Left">
<Setter Property="Orientation"
TargetName="HeaderPanel"
Value="Vertical" />
<Setter Property="Grid.Row"
TargetName="HeaderPanel"
Value="0" />
<Setter Property="Grid.Row"
TargetName="ContentPanel"
Value="0" />
<Setter Property="Grid.Column"
TargetName="HeaderPanel"
Value="0" />
<Setter Property="Grid.Column"
TargetName="ContentPanel"
Value="1" />
<Setter Property="Width"
TargetName="ColumnDefinition0"
Value="Auto" />
<Setter Property="Width"
TargetName="ColumnDefinition1"
Value="*" />
<Setter Property="Height"
TargetName="RowDefinition0"
Value="*" />
<Setter Property="Height"
TargetName="RowDefinition1"
Value="0" />
<Setter Property="Margin"
TargetName="HeaderPanel"
Value="2,2,0,2" />
</Trigger>
<Trigger Property="TabStripPlacement"
Value="Right">
<Setter Property="Orientation"
TargetName="HeaderPanel"
Value="Vertical" />
<Setter Property="Grid.Row"
TargetName="HeaderPanel"
Value="0" />
<Setter Property="Grid.Row"
TargetName="ContentPanel"
Value="0" />
<Setter Property="Grid.Column"
TargetName="HeaderPanel"
Value="1" />
<Setter Property="Grid.Column"
TargetName="ContentPanel"
Value="0" />
<Setter Property="Width"
TargetName="ColumnDefinition0"
Value="*" />
<Setter Property="Width"
TargetName="ColumnDefinition1"
Value="Auto" />
<Setter Property="Height"
TargetName="RowDefinition0"
Value="*" />
<Setter Property="Height"
TargetName="RowDefinition1"
Value="0" />
<Setter Property="Margin"
TargetName="HeaderPanel"
Value="0,2,2,2" />
</Trigger>
<Trigger Property="IsEnabled"
Value="false">
<Setter Property="Foreground"
Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Control.Template>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Caption}" />
<Button Content="x"
Grid.Column="2"
VerticalAlignment="Top"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</TabControl>
Code in the relevant view model looks like this:
public ICommand AddNewItemCommand
{
get
{
return new DelegateCommand((param) =>
{
MyItemSource.Add(CreateMyValueViewModel());
},
(param) => MyItemSource != null);
}
}
Pay attention: I wrapped TabPanel by StackPanel to flip the "+" button together with TabPanel regarding to value of property "TabStripPlacement". Without inheritance and without code-behind in your view.
I believe I have come up with a complete solution, I started with NVM's solution to create my template. And then referenced the DataGrid source code to come up with an extended TabControl capable of adding and removing items.
ExtendedTabControl.cs
public class ExtendedTabControl : TabControl
{
public static readonly DependencyProperty CanUserAddTabsProperty = DependencyProperty.Register("CanUserAddTabs", typeof(bool), typeof(ExtendedTabControl), new PropertyMetadata(false, OnCanUserAddTabsChanged, OnCoerceCanUserAddTabs));
public bool CanUserAddTabs
{
get { return (bool)GetValue(CanUserAddTabsProperty); }
set { SetValue(CanUserAddTabsProperty, value); }
}
public static readonly DependencyProperty CanUserDeleteTabsProperty = DependencyProperty.Register("CanUserDeleteTabs", typeof(bool), typeof(ExtendedTabControl), new PropertyMetadata(true, OnCanUserDeleteTabsChanged, OnCoerceCanUserDeleteTabs));
public bool CanUserDeleteTabs
{
get { return (bool)GetValue(CanUserDeleteTabsProperty); }
set { SetValue(CanUserDeleteTabsProperty, value); }
}
public static RoutedUICommand DeleteCommand
{
get { return ApplicationCommands.Delete; }
}
public static readonly DependencyProperty NewTabCommandProperty = DependencyProperty.Register("NewTabCommand", typeof(ICommand), typeof(ExtendedTabControl));
public ICommand NewTabCommand
{
get { return (ICommand)GetValue(NewTabCommandProperty); }
set { SetValue(NewTabCommandProperty, value); }
}
private IEditableCollectionView EditableItems
{
get { return (IEditableCollectionView)Items; }
}
private bool ItemIsSelected
{
get
{
if (this.SelectedItem != CollectionView.NewItemPlaceholder)
return true;
return false;
}
}
private static void OnCanExecuteDelete(object sender, CanExecuteRoutedEventArgs e)
{
((ExtendedTabControl)sender).OnCanExecuteDelete(e);
}
private static void OnCanUserAddTabsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((ExtendedTabControl)d).UpdateNewItemPlaceholder();
}
private static void OnCanUserDeleteTabsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// The Delete command needs to have CanExecute run.
CommandManager.InvalidateRequerySuggested();
}
private static object OnCoerceCanUserAddTabs(DependencyObject d, object baseValue)
{
return ((ExtendedTabControl)d).OnCoerceCanUserAddOrDeleteTabs((bool)baseValue, true);
}
private static object OnCoerceCanUserDeleteTabs(DependencyObject d, object baseValue)
{
return ((ExtendedTabControl)d).OnCoerceCanUserAddOrDeleteTabs((bool)baseValue, false);
}
private static void OnExecutedDelete(object sender, ExecutedRoutedEventArgs e)
{
((ExtendedTabControl)sender).OnExecutedDelete(e);
}
private static void OnSelectionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue == CollectionView.NewItemPlaceholder)
{
var tc = (ExtendedTabControl)d;
tc.Items.MoveCurrentTo(e.OldValue);
tc.Items.Refresh();
}
}
static ExtendedTabControl()
{
Type ownerType = typeof(ExtendedTabControl);
DefaultStyleKeyProperty.OverrideMetadata(ownerType, new FrameworkPropertyMetadata(typeof(ExtendedTabControl)));
SelectedItemProperty.OverrideMetadata(ownerType, new FrameworkPropertyMetadata(OnSelectionChanged));
CommandManager.RegisterClassCommandBinding(ownerType, new CommandBinding(DeleteCommand, new ExecutedRoutedEventHandler(OnExecutedDelete), new CanExecuteRoutedEventHandler(OnCanExecuteDelete)));
}
protected virtual void OnCanExecuteDelete(CanExecuteRoutedEventArgs e)
{
// User is allowed to delete and there is a selection.
e.CanExecute = CanUserDeleteTabs && ItemIsSelected;
e.Handled = true;
}
protected virtual void OnExecutedDelete(ExecutedRoutedEventArgs e)
{
if (ItemIsSelected)
{
int indexToSelect = -1;
object currentItem = e.Parameter ?? this.SelectedItem;
if (currentItem == this.SelectedItem)
indexToSelect = Math.Max(this.Items.IndexOf(currentItem) - 1, 0);
if (currentItem != CollectionView.NewItemPlaceholder)
EditableItems.Remove(currentItem);
if (indexToSelect != -1)
{
// This should focus the row and bring it into view.
SetCurrentValue(SelectedItemProperty, this.Items[indexToSelect]);
}
}
e.Handled = true;
}
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{
base.OnItemsSourceChanged(oldValue, newValue);
CoerceValue(CanUserAddTabsProperty);
CoerceValue(CanUserDeleteTabsProperty);
UpdateNewItemPlaceholder();
}
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
if (Keyboard.FocusedElement is TextBox)
Keyboard.FocusedElement.RaiseEvent(new RoutedEventArgs(LostFocusEvent));
base.OnSelectionChanged(e);
}
private bool OnCoerceCanUserAddOrDeleteTabs(bool baseValue, bool canUserAddTabsProperty)
{
// Only when the base value is true do we need to validate
// that the user can actually add or delete rows.
if (baseValue)
{
if (!this.IsEnabled)
{
// Disabled TabControls cannot be modified.
return false;
}
else
{
if ((canUserAddTabsProperty && !this.EditableItems.CanAddNew) || (!canUserAddTabsProperty && !this.EditableItems.CanRemove))
{
// The collection view does not allow the add or delete action.
return false;
}
}
}
return baseValue;
}
private void UpdateNewItemPlaceholder()
{
var editableItems = EditableItems;
if (CanUserAddTabs)
{
// NewItemPlaceholderPosition isn't a DP but we want to default to AtEnd instead of None
// (can only be done when canUserAddRows becomes true). This may override the users intent
// to make it None, however they can work around this by resetting it to None after making
// a change which results in canUserAddRows becoming true.
if (editableItems.NewItemPlaceholderPosition == NewItemPlaceholderPosition.None)
editableItems.NewItemPlaceholderPosition = NewItemPlaceholderPosition.AtEnd;
}
else
{
if (editableItems.NewItemPlaceholderPosition != NewItemPlaceholderPosition.None)
editableItems.NewItemPlaceholderPosition = NewItemPlaceholderPosition.None;
}
// Make sure the newItemPlaceholderRow reflects the correct visiblity
TabItem newItemPlaceholderTab = (TabItem)ItemContainerGenerator.ContainerFromItem(CollectionView.NewItemPlaceholder);
if (newItemPlaceholderTab != null)
newItemPlaceholderTab.CoerceValue(VisibilityProperty);
}
}
CustomStyleSelector.cs
internal class CustomStyleSelector : StyleSelector
{
public Style NewItemStyle { get; set; }
public override Style SelectStyle(object item, DependencyObject container)
{
if (item == CollectionView.NewItemPlaceholder)
return NewItemStyle;
else
return Application.Current.FindResource(typeof(TabItem)) as Style;
}
}
TemplateSelector.cs
internal class TemplateSelector : DataTemplateSelector
{
public DataTemplate ItemTemplate { get; set; }
public DataTemplate NewItemTemplate { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
if (item == CollectionView.NewItemPlaceholder)
return NewItemTemplate;
else
return ItemTemplate;
}
}
Generic.xaml
<!-- This style explains how to style a NewItemPlaceholder. -->
<Style x:Key="NewTabItemStyle" TargetType="{x:Type TabItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabItem}">
<ContentPresenter ContentSource="Header" HorizontalAlignment="Left" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- This template explains how to render a tab item with a close button. -->
<DataTemplate x:Key="ClosableTabItemHeader">
<DockPanel MinWidth="120">
<Button DockPanel.Dock="Right" Command="ApplicationCommands.Delete" CommandParameter="{Binding}" Content="X" Cursor="Hand" Focusable="False" FontSize="10" FontWeight="Bold" Height="16" Width="16" />
<TextBlock Padding="0,0,10,0" Text="{Binding DisplayName}" VerticalAlignment="Center" />
</DockPanel>
</DataTemplate>
<!-- This template explains how to render a tab item with a new button. -->
<DataTemplate x:Key="NewTabItemHeader">
<Button Command="{Binding NewTabCommand, RelativeSource={RelativeSource AncestorType={x:Type local:ExtendedTabControl}}}" Content="+" Cursor="Hand" Focusable="False" FontWeight="Bold"
Width="{Binding ActualHeight, RelativeSource={RelativeSource Self}}"/>
</DataTemplate>
<local:CustomStyleSelector x:Key="StyleSelector" NewItemStyle="{StaticResource NewTabItemStyle}" />
<local:TemplateSelector x:Key="HeaderTemplateSelector" ItemTemplate="{StaticResource ClosableTabItemHeader}" NewItemTemplate="{StaticResource NewTabItemHeader}" />
<Style x:Key="{x:Type local:ExtendedTabControl}" BasedOn="{StaticResource {x:Type TabControl}}" TargetType="{x:Type local:ExtendedTabControl}">
<Setter Property="ItemContainerStyleSelector" Value="{StaticResource StyleSelector}" />
<Setter Property="ItemTemplateSelector" Value="{StaticResource HeaderTemplateSelector}" />
</Style>
Define the ControlTemplate of the TabControl like this:
<!-- Sets the look of the Tabcontrol. -->
<Style x:Key="TabControlStyle" TargetType="{x:Type TabControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabControl}">
<Grid>
<!-- Upperrow holds the tabs themselves and lower the content of the tab -->
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
The upper row in the grid would be the TabPanel, but you would put that into a StackPanel with a button following the TabPanel, and style the button to look like a tab.
Now the button would create a new TabItem (your custom-created one perhaps) and add it to the ObservableCollection of Tabs you have as the Itemssource for your TabControl.
2 & 3) It should always appear at the end, and it's not a tab so hopefully not part of tab cycling
4) Well, your TabControl should use a ObservableCollection of TabItems as Itemssource to be notified when a new one is added/removed
Some code:
The NewTabButton usercontrol .cs file
public partial class NewTabButton : TabItem
{
public NewTabButton()
{
InitializeComponent();
Header = "+";
}
}
And the main window:
public partial class Window1 : Window
{
public ObservableCollection<TabItem> Tabs { get; set; }
public Window1()
{
InitializeComponent();
Tabs = new ObservableCollection<TabItem>();
for (int i = 0; i < 20; i++)
{
TabItem tab = new TabItem();
tab.Header = "TabNumber" + i.ToString();
Tabs.Add(tab);
}
Tabs.Add(new NewTabButton());
theTabs.ItemsSource = Tabs;
}
}
Now we would need to find a way to let it always appear bottom right and also add the event and style for it (the plus sign is there as a placeholder).
This would likely be better as a comment on #NVM's own solution; but I don't have the rep to comment yet so...
If you are trying to use the accepted solution and not getting the add command to trigger then you probably don't have a usercontrol named "parentUserControl".
You can alter #NVM's TabControl declaration as follows to make it work:
<TabControl x:Name="parentUserControl"
ItemsSource="{Binding Items}"
ItemTemplateSelector="{StaticResource headerTemplateSelector}"
ContentTemplateSelector="{StaticResource contentTemplateSelector}"/>
Obviously not a good name to give a tab control :); but I guess #NVM had the data context hooked further up his visual tree to an element to match the name.
Note that personally I preferred to use a relative binding by changing the following:
<Button Content="+"
Command="{Binding ElementName=parentUserControl,
Path=DataContext.NewCommand}"/>
To this:
<Button Content="+"
Command="{Binding DataContext.NewCommand,
RelativeSource={RelativeSource AncestorType={x:Type TabControl}}}"/>
In addition to NVM's answer.
I don't use so many templates and selector's for NewItemPlaceholder. Easier solution with no empty content:
<TabControl.ItemContainerStyle>
<Style TargetType="TabItem">
<Style.Triggers>
<DataTrigger Binding="{Binding}" Value="{x:Static CollectionView.NewItemPlaceholder}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Button Command="{Binding DataContext.AddPageCommand, RelativeSource={RelativeSource AncestorType={x:Type TabControl}}}"
HorizontalContentAlignment="Center" VerticalContentAlignment="Center" ToolTip="Add page" >
+
</Button>
</ControlTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</TabControl.ItemContainerStyle>
Ctrl+Tab I desided to disable. It's not SO easy, you should subscribe on KeyDown on parent element, i.e. Window (Ctrl+Shift+Tab also handled correctly):
public View()
{
InitializeComponent();
AddHandler(Keyboard.PreviewKeyDownEvent, (KeyEventHandler)controlKeyDownEvent);
}
private void controlKeyDownEvent(object sender, KeyEventArgs e)
{
e.Handled = e.Key == Key.Tab && Keyboard.Modifiers.HasFlag(ModifierKeys.Control);
}
To complete the answer given by #NVM what you have to add is the PreviewMouseDown event:
<TabControl PreviewMouseDown="ActionTabs_PreviewMouseDown"
</TabControl>
And then:
private void ActionTabs_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
ouseButtonEventArgs args = e as MouseButtonEventArgs;
FrameworkElement source = (FrameworkElement)args.OriginalSource;
if (source.DataContext.ToString() == "{NewItemPlaceholder}")
{
e.Handled = true;
}
}

Customizing the TreeView to allow multi select

The built-in WPF TreeView control does not allow for multi selection, like a ListBox does. How can I customize the TreeView to allow for multi selection without rewriting it.
I have a variation on SoMoS implementation that uses an attached property declared on a derivation of the base TreeView control to track the selection state of the TreeViewItems. This keeps the selection tracking on the TreeViewItem element itself, and off of the model object being presented by the tree-view.
This is the new TreeView class derivation.
using System.Linq;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using System.Windows.Controls;
using System.Collections;
using System.Collections.Generic;
namespace MultiSelectTreeViewDemo
{
public sealed class MultiSelectTreeView : TreeView
{
#region Fields
// Used in shift selections
private TreeViewItem _lastItemSelected;
#endregion Fields
#region Dependency Properties
public static readonly DependencyProperty IsItemSelectedProperty =
DependencyProperty.RegisterAttached("IsItemSelected", typeof(bool), typeof(MultiSelectTreeView));
public static void SetIsItemSelected(UIElement element, bool value)
{
element.SetValue(IsItemSelectedProperty, value);
}
public static bool GetIsItemSelected(UIElement element)
{
return (bool)element.GetValue(IsItemSelectedProperty);
}
#endregion Dependency Properties
#region Properties
private static bool IsCtrlPressed
{
get { return Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl); }
}
private static bool IsShiftPressed
{
get { return Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift); }
}
public IList SelectedItems
{
get
{
var selectedTreeViewItems = GetTreeViewItems(this, true).Where(GetIsItemSelected);
var selectedModelItems = selectedTreeViewItems.Select(treeViewItem => treeViewItem.Header);
return selectedModelItems.ToList();
}
}
#endregion Properties
#region Event Handlers
protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
{
base.OnPreviewMouseDown(e);
// If clicking on a tree branch expander...
if (e.OriginalSource is Shape || e.OriginalSource is Grid || e.OriginalSource is Border)
return;
var item = GetTreeViewItemClicked((FrameworkElement)e.OriginalSource);
if (item != null) SelectedItemChangedInternal(item);
}
#endregion Event Handlers
#region Utility Methods
private void SelectedItemChangedInternal(TreeViewItem tvItem)
{
// Clear all previous selected item states if ctrl is NOT being held down
if (!IsCtrlPressed)
{
var items = GetTreeViewItems(this, true);
foreach (var treeViewItem in items)
SetIsItemSelected(treeViewItem, false);
}
// Is this an item range selection?
if (IsShiftPressed && _lastItemSelected != null)
{
var items = GetTreeViewItemRange(_lastItemSelected, tvItem);
if (items.Count > 0)
{
foreach (var treeViewItem in items)
SetIsItemSelected(treeViewItem, true);
_lastItemSelected = items.Last();
}
}
// Otherwise, individual selection
else
{
SetIsItemSelected(tvItem, true);
_lastItemSelected = tvItem;
}
}
private static TreeViewItem GetTreeViewItemClicked(DependencyObject sender)
{
while (sender != null && !(sender is TreeViewItem))
sender = VisualTreeHelper.GetParent(sender);
return sender as TreeViewItem;
}
private static List<TreeViewItem> GetTreeViewItems(ItemsControl parentItem, bool includeCollapsedItems, List<TreeViewItem> itemList = null)
{
if (itemList == null)
itemList = new List<TreeViewItem>();
for (var index = 0; index < parentItem.Items.Count; index++)
{
var tvItem = parentItem.ItemContainerGenerator.ContainerFromIndex(index) as TreeViewItem;
if (tvItem == null) continue;
itemList.Add(tvItem);
if (includeCollapsedItems || tvItem.IsExpanded)
GetTreeViewItems(tvItem, includeCollapsedItems, itemList);
}
return itemList;
}
private List<TreeViewItem> GetTreeViewItemRange(TreeViewItem start, TreeViewItem end)
{
var items = GetTreeViewItems(this, false);
var startIndex = items.IndexOf(start);
var endIndex = items.IndexOf(end);
var rangeStart = startIndex > endIndex || startIndex == -1 ? endIndex : startIndex;
var rangeCount = startIndex > endIndex ? startIndex - endIndex + 1 : endIndex - startIndex + 1;
if (startIndex == -1 && endIndex == -1)
rangeCount = 0;
else if (startIndex == -1 || endIndex == -1)
rangeCount = 1;
return rangeCount > 0 ? items.GetRange(rangeStart, rangeCount) : new List<TreeViewItem>();
}
#endregion Utility Methods
}
}
And here's the XAML. Make note that the salient part is the replacement of the two triggers that use the singular 'IsSelected' property with the new 'IsItemSelected' attached property in the MultiSelectTreeViewItemStyle to achieve the visual state.
Also note I'm not aggregating the new TreeView control into a UserControl.
<Window
x:Class="MultiSelectTreeViewDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MultiSelectTreeViewDemo"
Title="MultiSelect TreeView Demo" Height="350" Width="525">
<Window.Resources>
<local:DemoViewModel x:Key="ViewModel"/>
<Style x:Key="TreeViewItemFocusVisual">
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate>
<Rectangle/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<SolidColorBrush x:Key="TreeViewItem.TreeArrow.Static.Checked.Fill" Color="#FF595959"/>
<SolidColorBrush x:Key="TreeViewItem.TreeArrow.Static.Checked.Stroke" Color="#FF262626"/>
<SolidColorBrush x:Key="TreeViewItem.TreeArrow.MouseOver.Stroke" Color="#FF1BBBFA"/>
<SolidColorBrush x:Key="TreeViewItem.TreeArrow.MouseOver.Fill" Color="Transparent"/>
<SolidColorBrush x:Key="TreeViewItem.TreeArrow.MouseOver.Checked.Stroke" Color="#FF262626"/>
<SolidColorBrush x:Key="TreeViewItem.TreeArrow.MouseOver.Checked.Fill" Color="#FF595959"/>
<PathGeometry x:Key="TreeArrow" Figures="M0,0 L0,6 L6,0 z"/>
<SolidColorBrush x:Key="TreeViewItem.TreeArrow.Static.Fill" Color="Transparent"/>
<SolidColorBrush x:Key="TreeViewItem.TreeArrow.Static.Stroke" Color="#FF989898"/>
<Style x:Key="ExpandCollapseToggleStyle" TargetType="{x:Type ToggleButton}">
<Setter Property="Focusable" Value="False"/>
<Setter Property="Width" Value="16"/>
<Setter Property="Height" Value="16"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<Border Background="Transparent" Height="16" Padding="5,5,5,5" Width="16">
<Path x:Name="ExpandPath" Data="{StaticResource TreeArrow}" Fill="{StaticResource TreeViewItem.TreeArrow.Static.Fill}" Stroke="{StaticResource TreeViewItem.TreeArrow.Static.Stroke}">
<Path.RenderTransform>
<RotateTransform Angle="135" CenterY="3" CenterX="3"/>
</Path.RenderTransform>
</Path>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter Property="RenderTransform" TargetName="ExpandPath">
<Setter.Value>
<RotateTransform Angle="180" CenterY="3" CenterX="3"/>
</Setter.Value>
</Setter>
<Setter Property="Fill" TargetName="ExpandPath" Value="{StaticResource TreeViewItem.TreeArrow.Static.Checked.Fill}"/>
<Setter Property="Stroke" TargetName="ExpandPath" Value="{StaticResource TreeViewItem.TreeArrow.Static.Checked.Stroke}"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Stroke" TargetName="ExpandPath" Value="{StaticResource TreeViewItem.TreeArrow.MouseOver.Stroke}"/>
<Setter Property="Fill" TargetName="ExpandPath" Value="{StaticResource TreeViewItem.TreeArrow.MouseOver.Fill}"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True"/>
<Condition Property="IsChecked" Value="True"/>
</MultiTrigger.Conditions>
<Setter Property="Stroke" TargetName="ExpandPath" Value="{StaticResource TreeViewItem.TreeArrow.MouseOver.Checked.Stroke}"/>
<Setter Property="Fill" TargetName="ExpandPath" Value="{StaticResource TreeViewItem.TreeArrow.MouseOver.Checked.Fill}"/>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="MultiSelectTreeViewItemStyle" TargetType="{x:Type TreeViewItem}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="HorizontalContentAlignment" Value="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
<Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
<Setter Property="Padding" Value="1,0,0,0"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
<Setter Property="FocusVisualStyle" Value="{StaticResource TreeViewItemFocusVisual}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TreeViewItem}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition MinWidth="19" Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<ToggleButton
x:Name="Expander"
ClickMode="Press"
IsChecked="{Binding IsExpanded, RelativeSource={RelativeSource TemplatedParent}}"
Style="{StaticResource ExpandCollapseToggleStyle}"/>
<Border
x:Name="Bd"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
Grid.Column="1"
Padding="{TemplateBinding Padding}"
SnapsToDevicePixels="true">
<ContentPresenter
x:Name="PART_Header"
ContentSource="Header"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</Border>
<ItemsPresenter
x:Name="ItemsHost"
Grid.ColumnSpan="2"
Grid.Column="1"
Grid.Row="1"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="false">
<Setter Property="Visibility" TargetName="ItemsHost" Value="Collapsed"/>
</Trigger>
<Trigger Property="HasItems" Value="false">
<Setter Property="Visibility" TargetName="Expander" Value="Hidden"/>
</Trigger>
<!--Trigger Property="IsSelected" Value="true"-->
<Trigger Property="local:MultiSelectTreeView.IsItemSelected" Value="true">
<Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<!--Condition Property="IsSelected" Value="true"/-->
<Condition Property="local:MultiSelectTreeView.IsItemSelected" Value="true"/>
<Condition Property="IsSelectionActive" Value="false"/>
</MultiTrigger.Conditions>
<Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.InactiveSelectionHighlightBrushKey}}"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.InactiveSelectionHighlightTextBrushKey}}"/>
</MultiTrigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="VirtualizingPanel.IsVirtualizing" Value="true">
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<VirtualizingStackPanel/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid
Background="WhiteSmoke"
DataContext="{DynamicResource ViewModel}">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<local:MultiSelectTreeView
x:Name="multiSelectTreeView"
ItemContainerStyle="{StaticResource MultiSelectTreeViewItemStyle}"
ItemsSource="{Binding FoodGroups}">
<local:MultiSelectTreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<Grid>
<TextBlock FontSize="14" Text="{Binding Name}"/>
</Grid>
</HierarchicalDataTemplate>
</local:MultiSelectTreeView.ItemTemplate>
</local:MultiSelectTreeView>
<Button
Grid.Row="1"
Margin="0,10"
Padding="20,2"
HorizontalAlignment="Center"
Content="Get Selections"
Click="GetSelectionsButton_OnClick"/>
</Grid>
</Window>
And here's a cheesy view-model to drive it (for demo purposes).
using System.Collections.ObjectModel;
namespace MultiSelectTreeViewDemo
{
public sealed class DemoViewModel
{
public ObservableCollection<FoodItem> FoodGroups { get; set; }
public DemoViewModel()
{
var redMeat = new FoodItem { Name = "Reds" };
redMeat.Add(new FoodItem { Name = "Beef" });
redMeat.Add(new FoodItem { Name = "Buffalo" });
redMeat.Add(new FoodItem { Name = "Lamb" });
var whiteMeat = new FoodItem { Name = "Whites" };
whiteMeat.Add(new FoodItem { Name = "Chicken" });
whiteMeat.Add(new FoodItem { Name = "Duck" });
whiteMeat.Add(new FoodItem { Name = "Pork" });
var meats = new FoodItem { Name = "Meats", Children = { redMeat, whiteMeat } };
var veggies = new FoodItem { Name = "Vegetables" };
veggies.Add(new FoodItem { Name = "Potato" });
veggies.Add(new FoodItem { Name = "Corn" });
veggies.Add(new FoodItem { Name = "Spinach" });
var fruits = new FoodItem { Name = "Fruits" };
fruits.Add(new FoodItem { Name = "Apple" });
fruits.Add(new FoodItem { Name = "Orange" });
fruits.Add(new FoodItem { Name = "Pear" });
FoodGroups = new ObservableCollection<FoodItem> { meats, veggies, fruits };
}
}
public sealed class FoodItem
{
public string Name { get; set; }
public ObservableCollection<FoodItem> Children { get; set; }
public FoodItem()
{
Children = new ObservableCollection<FoodItem>();
}
public void Add(FoodItem item)
{
Children.Add(item);
}
}
}
And here's the button click-handler on the MainWindow code-behind that shows the selections in a MessageBox.
private void GetSelectionsButton_OnClick(object sender, RoutedEventArgs e)
{
var selectedMesg = "";
var selectedItems = multiSelectTreeView.SelectedItems;
if (selectedItems.Count > 0)
{
selectedMesg = selectedItems.Cast<FoodItem>()
.Where(modelItem => modelItem != null)
.Aggregate(selectedMesg, (current, modelItem) => current + modelItem.Name + Environment.NewLine);
}
else
selectedMesg = "No selected items!";
MessageBox.Show(selectedMesg, "MultiSelect TreeView Demo", MessageBoxButton.OK);
}
Hope this helps.
When I consider overriding the fundamental behavior of a control, like a treeview, I always like to consider the usability and effort associated with my decision.
In the specific case of a treeview I find that switching to a listview in combination with zero, one, or more controls makes for a more usable solution that often is easier to implement.
As an example, consider the common Open dialog, or Windows Explorer application.
I've simplified this task adding a checkbox before the text for each treeviewitem.
So, I've created a dockpanel with 2 items inside: checkbox + textblock.
So...
XAML
<TreeView x:Name="treeViewProcesso" Margin="1,30.351,1,5" BorderBrush="{x:Null}" MinHeight="250" VerticalContentAlignment="Top" BorderThickness="0" >
<TreeViewItem Header="Documents" x:Name="treeView" IsExpanded="True" DisplayMemberPath="DocumentsId" >
</TreeViewItem>
</TreeView>
CS
TreeViewItem treeViewItem = new TreeViewItem();
DockPanel dp = new DockPanel();
CheckBox cb = new CheckBox();
TextBlock tb = new TextBlock();
tb.Text = "Item";
dp.Children.Add(cb);
dp.Children.Add(tb);
treeViewItem.Header = dp;
treeViewItem.Selected += new RoutedEventHandler(item_Selected);
treeView.Items.Add(treeViewItem);
And then you can access checkbox value:
void item_Selected(object sender, RoutedEventArgs e)
{
selectedTVI = ((TreeViewItem)sender);
CheckBox cb = (Checkbox)((DockPanel)selectedTVI.Header).Children[0];
}
This is a simple way to do if you don't need anything complex.
I finally ended coding my own CustomControl containing a TreeView inside. Based on the work of others the key of the functionality resides on making all the items of the Model of the TreeView inherit the interface ISelectable:
public interface ISelectable
{
public bool IsSelected {get; set}
}
This way we will have a new 'IsSelected' property that has nothing to do with the TreeViewItem IsSelected. We just need to style our tree so it handles the model IsSelected property. Here the code (it's using the Drag & drop libraries available at http://code.google.com/p/gong-wpf-dragdrop/):
XAML
<UserControl x:Class="Picis.Wpf.Framework.ExtendedControls.TreeViewEx.TreeViewEx"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:DragAndDrop="clr-namespace:Picis.Wpf.Framework.DragAndDrop">
<TreeView ItemsSource="{Binding ItemsSource, RelativeSource={RelativeSource AncestorType=UserControl}}"
ItemTemplate="{Binding ItemTemplate, RelativeSource={RelativeSource AncestorType=UserControl}}"
ItemContainerStyle="{Binding ItemContainerStyle, RelativeSource={RelativeSource AncestorType=UserControl}}"
DragAndDrop:DragDrop.DropHandler ="{Binding DropHandler, RelativeSource={RelativeSource AncestorType=UserControl}}"
PreviewMouseDown="TreeViewOnPreviewMouseDown"
PreviewMouseUp="TreeViewOnPreviewMouseUp"
x:FieldModifier="private" x:Name="InnerTreeView" >
<TreeView.Resources>
<Style TargetType="TreeViewItem">
<Style.Resources>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="White" />
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightTextBrushKey}" Color="Black" />
<SolidColorBrush x:Key="{x:Static SystemColors.ControlBrushKey}" Color="White" />
</Style.Resources>
</Style>
</TreeView.Resources>
</TreeView>
C#:
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using GongSolutions.Wpf.DragDrop;
using DragDrop = GongSolutions.Wpf.DragDrop;
namespace <yournamespace>.TreeViewEx
{
public partial class TreeViewEx : UserControl
{
#region Attributes
private TreeViewItem _lastItemSelected; // Used in shift selections
private TreeViewItem _itemToCheck; // Used when clicking on a selected item to check if we want to deselect it or to drag the current selection
private bool _isDragEnabled;
private bool _isDropEnabled;
#endregion
#region Dependency Properties
public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register("ItemsSource", typeof(IEnumerable<ISelectable>), typeof(TreeViewEx));
public IEnumerable<ISelectable> ItemsSource
{
get
{
return (IEnumerable<ISelectable>)this.GetValue(TreeViewEx.ItemsSourceProperty);
}
set
{
this.SetValue(TreeViewEx.ItemsSourceProperty, value);
}
}
public static readonly DependencyProperty ItemTemplateProperty = DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(TreeViewEx));
public DataTemplate ItemTemplate
{
get
{
return (DataTemplate)GetValue(TreeViewEx.ItemTemplateProperty);
}
set
{
SetValue(TreeViewEx.ItemTemplateProperty, value);
}
}
public static readonly DependencyProperty ItemContainerStyleProperty = DependencyProperty.Register("ItemContainerStyle", typeof(Style), typeof(TreeViewEx));
public Style ItemContainerStyle
{
get
{
return (Style)GetValue(TreeViewEx.ItemContainerStyleProperty);
}
set
{
SetValue(TreeViewEx.ItemContainerStyleProperty, value);
}
}
public static readonly DependencyProperty DropHandlerProperty = DependencyProperty.Register("DropHandler", typeof(IDropTarget), typeof(TreeViewEx));
public IDropTarget DropHandler
{
get
{
return (IDropTarget)GetValue(TreeViewEx.DropHandlerProperty);
}
set
{
SetValue(TreeViewEx.DropHandlerProperty, value);
}
}
#endregion
#region Properties
public bool IsDragEnabled
{
get
{
return _isDragEnabled;
}
set
{
if (_isDragEnabled != value)
{
_isDragEnabled = value;
DragDrop.SetIsDragSource(this.InnerTreeView, _isDragEnabled);
}
}
}
public bool IsDropEnabled
{
get
{
return _isDropEnabled;
}
set
{
if (_isDropEnabled != value)
{
_isDropEnabled = value;
DragDrop.SetIsDropTarget(this.InnerTreeView, _isDropEnabled);
}
}
}
#endregion
#region Public Methods
public TreeViewEx()
{
InitializeComponent();
}
#endregion
#region Event Handlers
private void TreeViewOnPreviewMouseDown(object sender, MouseButtonEventArgs e)
{
if (e.OriginalSource is Shape || e.OriginalSource is Grid || e.OriginalSource is Border) // If clicking on the + of the tree
return;
TreeViewItem item = this.GetTreeViewItemClicked((FrameworkElement)e.OriginalSource);
if (item != null && item.Header != null)
{
this.SelectedItemChangedHandler(item);
}
}
// Check done to avoid deselecting everything when clicking to drag
private void TreeViewOnPreviewMouseUp(object sender, MouseButtonEventArgs e)
{
if (_itemToCheck != null)
{
TreeViewItem item = this.GetTreeViewItemClicked((FrameworkElement)e.OriginalSource);
if (item != null && item.Header != null)
{
if (!TreeViewEx.IsCtrlPressed)
{
GetTreeViewItems(true).Select(t => t.Header).Cast<ISelectable>().ToList().ForEach(f => f.IsSelected = false);
((ISelectable)_itemToCheck.Header).IsSelected = true;
_lastItemSelected = _itemToCheck;
}
else
{
((ISelectable)_itemToCheck.Header).IsSelected = false;
_lastItemSelected = null;
}
}
}
}
#endregion
#region Private Methods
private void SelectedItemChangedHandler(TreeViewItem item)
{
ISelectable content = (ISelectable)item.Header;
_itemToCheck = null;
if (content.IsSelected)
{
// Check it at the mouse up event to avoid deselecting everything when clicking to drag
_itemToCheck = item;
}
else
{
if (!TreeViewEx.IsCtrlPressed)
{
GetTreeViewItems(true).Select(t => t.Header).Cast<ISelectable>().ToList().ForEach(f => f.IsSelected = false);
}
if (TreeViewEx.IsShiftPressed && _lastItemSelected != null)
{
foreach (TreeViewItem tempItem in GetTreeViewItemsBetween(_lastItemSelected, item))
{
((ISelectable)tempItem.Header).IsSelected = true;
_lastItemSelected = tempItem;
}
}
else
{
content.IsSelected = true;
_lastItemSelected = item;
}
}
}
private static bool IsCtrlPressed
{
get
{
return Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl);
}
}
private static bool IsShiftPressed
{
get
{
return Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift);
}
}
private TreeViewItem GetTreeViewItemClicked(UIElement sender)
{
Point point = sender.TranslatePoint(new Point(0, 0), this.InnerTreeView);
DependencyObject visualItem = this.InnerTreeView.InputHitTest(point) as DependencyObject;
while (visualItem != null && !(visualItem is TreeViewItem))
{
visualItem = VisualTreeHelper.GetParent(visualItem);
}
return visualItem as TreeViewItem;
}
private IEnumerable<TreeViewItem> GetTreeViewItemsBetween(TreeViewItem start, TreeViewItem end)
{
List<TreeViewItem> items = this.GetTreeViewItems(false);
int startIndex = items.IndexOf(start);
int endIndex = items.IndexOf(end);
// It's possible that the start element has been removed after it was selected,
// I don't find a way to happen on the end but I add the code to handle the situation just in case
if (startIndex == -1 && endIndex == -1)
{
return new List<TreeViewItem>();
}
else if (startIndex == -1)
{
return new List<TreeViewItem>() {end};
}
else if (endIndex == -1)
{
return new List<TreeViewItem>() { start };
}
else
{
return startIndex > endIndex ? items.GetRange(endIndex, startIndex - endIndex + 1) : items.GetRange(startIndex, endIndex - startIndex + 1);
}
}
private List<TreeViewItem> GetTreeViewItems(bool includeCollapsedItems)
{
List<TreeViewItem> returnItems = new List<TreeViewItem>();
for (int index = 0; index < this.InnerTreeView.Items.Count; index++)
{
TreeViewItem item = (TreeViewItem)this.InnerTreeView.ItemContainerGenerator.ContainerFromIndex(index);
returnItems.Add(item);
if (includeCollapsedItems || item.IsExpanded)
{
returnItems.AddRange(GetTreeViewItemItems(item, includeCollapsedItems));
}
}
return returnItems;
}
private static IEnumerable<TreeViewItem> GetTreeViewItemItems(TreeViewItem treeViewItem, bool includeCollapsedItems)
{
List<TreeViewItem> returnItems = new List<TreeViewItem>();
for (int index = 0; index < treeViewItem.Items.Count; index++)
{
TreeViewItem item = (TreeViewItem)treeViewItem.ItemContainerGenerator.ContainerFromIndex(index);
if (item != null)
{
returnItems.Add(item);
if (includeCollapsedItems || item.IsExpanded)
{
returnItems.AddRange(GetTreeViewItemItems(item, includeCollapsedItems));
}
}
}
return returnItems;
}
#endregion
}
}

Resources