Show SelectedIndex in WPF Tabcontrol header template - c#

I have 1...n tabcontrols in my application, with the following XAML setup:
<TabControl Name="ordersTabControl" ItemsSource="{Binding CoilItems}">
<TabControl.ItemTemplate>
<DataTemplate DataType="models:Coil">
<StackPanel>
<TextBlock Text="{Binding CoilCode, StringFormat='Coil: {0}'}" />
<TextBlock Text="{Binding ArticleCode, StringFormat='Auftrag: {0}'}" />
<TextBlock Text="{Binding RestWeight, StringFormat='Restgewicht: {0} kg'}" />
</StackPanel>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
[...]
</TabControl.ContentTemplate>
</TabControl>
The amount of open tabs changes at runtime. Now I'd like to show an index in each tab (i.e. the first tab shows "Order 1", the second "Order 2" and so on) in addition to the information already in each header.
AFAIK when using DataTemplate I can't access the tab-properties through the code-behind, so is there any way in XAML to bind a textblock inside a tabheader to show the Index of that specific tab in the tabcontrol?
I think it should be possible with RelativeSource and FindAncestors? Alas I couldn't really find any clear tutorial on those settings (and I only started using WPF 2 days ago).

I'm going to give you a solution using attached properties. Check the code:
Attached Properties
public static class IndexAttachedProperty
{
#region TabItemIndex
public static int GetTabItemIndex(DependencyObject obj)
{
return (int) obj.GetValue(TabItemIndexProperty);
}
public static void SetTabItemIndex(DependencyObject obj, int value)
{
obj.SetValue(TabItemIndexProperty, value);
}
// Using a DependencyProperty as the backing store for TabItemIndex. This enables animation, styling, binding, etc...
public static readonly DependencyProperty TabItemIndexProperty =
DependencyProperty.RegisterAttached("TabItemIndex", typeof (int), typeof (IndexAttachedProperty),
new PropertyMetadata(-1));
#endregion
#region TrackTabItemIndex
public static bool GetTrackTabItemIndex(DependencyObject obj)
{
return (bool) obj.GetValue(TrackTabItemIndexProperty);
}
public static void SetTrackTabItemIndex(DependencyObject obj, bool value)
{
obj.SetValue(TrackTabItemIndexProperty, value);
}
// Using a DependencyProperty as the backing store for TrackTabItemIndex. This enables animation, styling, binding, etc...
public static readonly DependencyProperty TrackTabItemIndexProperty =
DependencyProperty.RegisterAttached("TrackTabItemIndex", typeof (bool), typeof (IndexAttachedProperty),
new PropertyMetadata(false, TrackTabItemIndexOnPropertyChanged));
private static void TrackTabItemIndexOnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var tabControl = GetParent(d, p => p is TabControl) as TabControl;
var tabItem = GetParent(d, p => p is TabItem) as TabItem;
if (tabControl == null || tabItem == null)
return;
if (!(bool)e.NewValue)
return;
int index = tabControl.Items.IndexOf(tabItem.DataContext == null ? tabItem : tabItem.DataContext);
SetTabItemIndex(d, index);
}
#endregion
public static DependencyObject GetParent(DependencyObject item, Func<DependencyObject, bool> condition)
{
if (item == null)
return null;
return condition(item) ? item : GetParent(VisualTreeHelper.GetParent(item), condition);
}
}
This code define two attached properties, the first one is to set if an item tracks the the tab item index in wich it is contained. The second one is the index property.
XAML Sample code:
<TabControl.ItemTemplate>
<DataTemplate DataType="{x:Type WpfApplication3:A}">
<StackPanel x:Name="tabItemRoot" WpfApplication3:IndexAttachedProperty.TrackTabItemIndex ="True">
<TextBlock Text="{Binding Text}"/>
<TextBlock Text="{Binding Path=(WpfApplication3:IndexAttachedProperty.TabItemIndex), ElementName=tabItemRoot}"/>
</StackPanel>
</DataTemplate>
</TabControl.ItemTemplate>
The above code is an example of using the attached property. You may adapt to your code easy.
Result:
Hope this code works for you...

If you are not using AlternationCount property for other purposes, you can hack it for an easier solution.
bind AlternationCount like this
<TabControl AlternationCount="{Binding Path=Items.Count, RelativeSource={RelativeSource Self}}">
And then in your ItemTemplate bind the TextBlock or other control you wish to the AlternationIndex like this,
<TextBlock Text="{Binding Path=(ItemsControl.AlternationIndex), RelativeSource={RelativeSource FindAncestor, AncestorType=TabItem}}" />
With a custom converter plugged into the above binding, you can display anything you want.

Even if you had access to the just the TabItem properties in the code-behind, it wouldn't help as the tab doesn't know it's own index in the TabControl collection. This is true of all ItemsControls and seems annoying, but it makes sense because when can any object ever tell you what its own position is within a collection?
It can be done with IndexOf, however, as long as you have access to both the control's ItemsCollection and the content of the tab. We can do this in a MultiValueConverter, so that it can be done within the DataTemplate.
Converter code:
public class ItemsControlIndexConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
ItemCollection itemCollection = (ItemCollection)values[0];
return (itemCollection.IndexOf(values[1]) + 1).ToString();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
TabControl XAML:
<TabControl ItemsSource="{Binding CoilItems}">
<TabControl.Resources>
<local:ItemsControlIndexConverter x:Key="IndexConverter"/>
</TabControl.Resources>
<TabControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource IndexConverter}" StringFormat="Order {0}" Mode="OneWay">
<Binding RelativeSource="{RelativeSource AncestorType=TabControl}" Path="Items"/> <!-- First converter index is the ItemsCollection -->
<Binding /> <!-- Second index is the content of this tab -->
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<!-- Fill in the rest of the header template -->
</StackPanel>
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>

Related

How can I bind properties of a view model to properties of items of a collection used as source for an items control

What I am trying to achieve is to bind properties of a ViewModel (mvvm light) object to some self made custom control in a grouped way.
So I created a CustomControl1, with a Title property and an ItemsCollection of my own custom type SomeDataControl.
public class CustomControl1 : Control
{
static CustomControl1()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomControl1), new FrameworkPropertyMetadata(typeof(CustomControl1)));
}
public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
"Text", typeof(string), typeof(CustomControl1), new PropertyMetadata(default(string)));
public string Text
{
get { return (string) GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
public ObservableCollection<SomeDataControl> _ItemsCollection;
public ObservableCollection<SomeDataControl> ItemsCollection {
get
{
if(_ItemsCollection == null) _ItemsCollection = new ObservableCollection<SomeDataControl>();
return _ItemsCollection;
}
}
}
public class SomeDataControl : DependencyObject
{
public static readonly DependencyProperty LAbelProperty = DependencyProperty.Register(
"LAbel", typeof(string), typeof(SomeDataControl), new PropertyMetadata(default(string)));
public string LAbel
{
get { return (string) GetValue(LAbelProperty); }
set { SetValue(LAbelProperty, value); }
}
public static readonly DependencyProperty DValueProperty = DependencyProperty.Register(
"DValue", typeof(double), typeof(SomeDataControl), new PropertyMetadata(default(double)));
public double DValue
{
get { return (double) GetValue(DValueProperty); }
set { SetValue(DValueProperty, value); }
}
}
I have also added a stlye to render the content of my control into an ItemsControl and bound the values to the appropriate fields like this:
<Style x:Key="ControlStyle" TargetType="local:CustomControl1">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<StackPanel>
<Label Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Text}"></Label>
<ItemsControl ItemsSource="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=ItemsCollection}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Label Content="{Binding Path=LAbel}" />
<Label Content="{Binding Path=DValue}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
And I put all this to my view with a view model as DataContext, so I can reach the values.
<Window x:Class="BindingTest.MainWindow" x:Name="thisControl" DataContext="{Binding Source={StaticResource Locator}, Path=VM}">
...
<local:CustomControl1 Text="{Binding Path=DataContext.Text, ElementName=thisControl}" Style="{StaticResource ControlStyle}" >
<local:CustomControl1.ItemsCollection>
<local:SomeDataControl LAbel="Apple" DValue="{Binding Path=DataContext.DVal1, Mode=TwoWay, ElementName=thisControl}">
<local:SomeDataControl LAbel="Peach" DValue="{Binding Path=DataContext.DVal2, Mode=TwoWay, ElementName=thisControl}">
<local:SomeDataControl LAbel="Pear" DValue="{Binding Path=DataContext.DVal3, Mode=TwoWay, ElementName=thisControl}"></local:SomeDataControl>
</local:CustomControl1.ItemsCollection>
</local:CustomControl1>
Everything goes fine, until I want to bind the DVal1,2 and 3 to the specific items. They are all with the default values.
I have been looking for the answer for 3 days already, but I could not find anything that would help. I tried also using DependenyProperty for the collection, or changing the type of it to a simple list, of also Freezable, but nothing helped at all.
I would really like to declare my groups this way in XAML and not putting everything together in my ViewModel to reach the layout.
Any kind of help would be great.
Thank you in advance.
Problem is your bindings
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Label Content="{Binding Path=LAbel}" />
<Label Content="{Binding Path=DValue}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>``
can not realize while you have Itemsource collection building in progress
<local:CustomControl1.ItemsCollection>
<local:SomeDataControl LAbel="Apple" DValue="{Binding Path=DataContext.DVal1, Mode=TwoWay, ElementName=thisControl}">
<local:SomeDataControl LAbel="Peach" DValue="{Binding Path=DataContext.DVal2, Mode=TwoWay, ElementName=thisControl}">
<local:SomeDataControl LAbel="Pear" DValue="{Binding Path=DataContext.DVal3, Mode=TwoWay, ElementName=thisControl}"></local:SomeDataControl>
</local:CustomControl1.ItemsCollection>
When you create first SomeDataControl ,Style expects an itemsource but its not available till the closing tag is reached.
So, if you dont want in Viewmodel ,create itemsource in Resources section of your window and bind it to your customControl.
<Window.Resources>
<x:Array x:Key="Mycollection" Type="local:SomeDataControl">
<local:SomeDataControl LAbel="Apple"
DValue="{Binding Path=DataContext.DVal1, Mode=TwoWay}"/>
<local:SomeDataControl LAbel="Peach"
DValue="{Binding Path=DataContext.DVal2, Mode=TwoWay}"/>
</x:Array>
</Window.Resources>
and bind to collection
<local:CustomControl1.ItemsCollection ItemsSource={StaticResource Mycollection}/>
Actually I found the answer by using both of the tips and some googling.
The main problem was, that the my SomeDataControl items were not part of the visual tree, therefore they did not get the datacontext of the higher level FrameworkElement.
So I introduced a Binding Proxy thank to this post:
Usage of Binding Proxy
So my XAML looks like this:
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Styles.xaml"></ResourceDictionary>
</ResourceDictionary.MergedDictionaries>
<local:BindingProxy x:Key="proxy" Data="{Binding}" />
</ResourceDictionary>
</Window.Resources>
...
<local:SomeDataControl LAbel="Apple" DValue="{Binding Path=Data.DVal1, Source={StaticResource proxy}}"></local:SomeDataControl>
and the code for the binding proxy is also very good and easy, it worth to re-share it:
public class BindingProxy : Freezable
{
#region Overrides of Freezable
protected override Freezable CreateInstanceCore()
{
return new BindingProxy();
}
#endregion
public object Data
{
get { return (object) GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
// Using a DependencyProperty as the backing store for Data. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
}
A little explanation also comes in the linked blog post
The solution to our problem is actually quite simple, and takes advantage of the Freezable class. The primary purpose of this class is to define objects that have a modifiable and a read-only state, but the interesting feature in our case is that Freezable objects can inherit the DataContext even when they’re not in the visual or logical tree.
Thank you all for your support.

WPF: Binding inside resources section - can't access DatatContext

I'm having a binding problem inside the resources section of a DataTemplate:
<Window
...
DataContext="{Binding RelativeSource={RelativeSource Self}}" >
...
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.Resources>
<DataTemplate x:Key="TextTemplate">
<DataTemplate.Resources>
<!-- Binding below doesn't seem to work -->
<local:FreezableProxyElement x:Key="Proxy" Data="{Binding}" />
<local:Message x:Key="Message" Text="{Binding Data, Source={StaticResource Proxy}}" />
</DataTemplate.Resources>
<StackPanel Orientation="Horizontal">
<Label>Text</Label>
<TextBlock Text="{Binding Text, Source={StaticResource Message}}" />
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="IntegerTemplate">
<StackPanel Orientation="Horizontal">
<Label>Integer</Label>
<TextBlock Text="{Binding}" />
</StackPanel>
</DataTemplate>
</ItemsControl.Resources>
<ItemsControl.ItemTemplateSelector>
<local:DataTypeSelector
TextTemplate="{StaticResource TextTemplate}"
IntegerTemplate="{StaticResource IntegerTemplate}"
/>
</ItemsControl.ItemTemplateSelector>
</ItemsControl>
Apologies for the length (and the contrived example) - I've tried to be as brief as possible. The important part is the Resources section of the first DataTemplate ("TextTemplate").
Supporting code behind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
public ObservableCollection<object> Items { get; } = new ObservableCollection<object>() { (int)5, "Hello World!" };
}
public class Message : DependencyObject
{
public string Text
{
get => (string)GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
public static readonly DependencyProperty TextProperty
= DependencyProperty.Register("Text", typeof(string), typeof(Message));
}
public class FreezableProxyElement : Freezable
{
protected override Freezable CreateInstanceCore()
=> new FreezableProxyElement();
public object Data
{
get => (object)GetValue(DataProperty);
set => SetValue(DataProperty, value);
}
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("Data", typeof(object), typeof(FreezableProxyElement));
}
public class DataTypeSelector : DataTemplateSelector
{
public DataTemplate TextTemplate { get; set; }
public DataTemplate IntegerTemplate { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
switch (item)
{
case string s:
return TextTemplate;
case int i:
return IntegerTemplate;
default:
return null;
}
}
}
I've always understood that in a resources section, a Freezable-derived object could inherit the DataContext from its container (which, in this DataTemplate, should be an item from ItemsControl.ItemsSource and this is verified by the collection's integer "5" being correctly displayed by "IntegerTemplate"). However, the text string is not displayed and the binding to "FreezeableProxyElement.Data" in the "TextTemplate" resources section yields a data error:
System.Windows.Data Error: 2 : Cannot find governing FrameworkElement or FrameworkContentElement for target element. BindingExpression:(no path); DataItem=null; target element is 'FreezableProxyElement' (HashCode=12379565); target property is 'Data' (type 'Object')
This is the error I would expect to see if FreezableProxyElement wasn't derived from Freezable. What's the problem here, and is there a good way around it?
I felt a disturbance in the Framework that led me to suspect that the DataTemplate's resources were at one too many removes from reality. I don't know exactly what the problem was (visual tree, logical tree, something or other), but I was able to reproduce it, and the following works for me:
<DataTemplate x:Key="TextTemplate">
<StackPanel Orientation="Horizontal">
<StackPanel.Resources>
<local:FreezableProxyElement
x:Key="Proxy"
Data="{Binding}"
/>
<local:Message
x:Key="Message"
Text="{Binding Data, Source={StaticResource Proxy}}"
/>
</StackPanel.Resources>
<Label>Text</Label>
<Label
Content="{Binding Text, Source={StaticResource Message}}"
/>
</StackPanel>
</DataTemplate>
I still suspect that this is an XY Problem, and that the real solution is to create Message some other way. But without knowing more details I can't give you that answer.
Update
How to do a binding trace:
Text="{Binding Data, Source={StaticResource Proxy}, PresentationTraceSources.TraceLevel=High}"
Don't leave those all over the place, they eat cycles.

WPF Customcontrol, Templates, Inheritance and Dependencyproperties

I've got some troubles with a custom control I need to create. I try to explain you my needs first
I need to have a combobox that permits to check more than one item at time (with checkbox) but I want it to be smart enought to bind to a specific type.
I've found some MultiSelectionComboBox but none reflects my need.
Btw my main problem is that I wish to have a generic class as
public class BaseClass<T> : BaseClass
{
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof(IEnumerable<T>), typeof(BaseClass<T>), new FrameworkPropertyMetadata(null,
new PropertyChangedCallback(BaseClass<T>.OnItemsSourceChanged)));
private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
int i = 0;
//MultiSelectComboBox control = (MultiSelectComboBox)d;
//control.DisplayInControl();
}
public IEnumerable<T> ItemsSource
{
get { return (IEnumerable<T>)GetValue(ItemsSourceProperty); }
set
{
SetValue(ItemsSourceProperty, value);
}
}
}
public class BaseClass : Control
{
}
and a more context specific item for example
public class MultiCurr : BaseClass<Currency>
{
static MultiCurr()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MultiCurr), new FrameworkPropertyMetadata(typeof(MultiCurr)));
}
}
In my App.xaml I've defined a resource as
<ResourceDictionary>
<Style TargetType="local:MultiCurr">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:MultiCurr">
<ComboBox Width="120" Background="Red" Height="30" ItemsSource="{Binding ItemsSource}" DisplayMemberPath="Description" ></ComboBox>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
In my MainWindow I've created an object as
<Grid>
<local:MultiCurr x:Name="test" ItemsSource="{Binding Currencies}"></local:MultiCurr>
</Grid>
and the MainWindow.cs is defined as
public partial class MainWindow : Window, INotifyPropertyChanged
{
private IList currencies;
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
this.Loaded += MainWindow_Loaded;
}
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
var lst = new List<Currency>();
for (int i = 0; i < 10; i++)
{
var curr = new Currency
{
ID = i,
Description = string.Format("Currency_{0}", i)
};
lst.Add(curr);
}
Currencies = lst;
}
public IList<Currency> Currencies
{
get
{
return this.currencies;
}
set
{
this.currencies = value;
NotifyPropertyChanged("Currencies");
}
}
public event PropertyChangedEventHandler PropertyChanged;
// This method is called by the Set accessor of each property.
// The CallerMemberName attribute that is applied to the optional propertyName
// parameter causes the property name of the caller to be substituted as an argument.
private void NotifyPropertyChanged(String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
And here's the result ...
I was wondering what am I doing wrong? is it possible what am I tring to achieve?
Thanks
UPDATE #1:
I've seen that the main problem is the datacontext of the custom usercontrol
<Application.Resources>
<ResourceDictionary>
<Style TargetType="local:MultiCurr">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:MultiCurr">
<ComboBox Width="120" Background="Red" Height="30" ItemsSource="{Binding **Currencies**}" DisplayMemberPath="{Binding **DisplayMemeberPath**}" ></ComboBox>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
</Application.Resources>
If I put ItemsSource as Currency (which is a property of the MainWindow) it shows.
If I put ItemsSource and DisplayMemberPath (which are defined in the BaseClass no.. how can I set the context of the usercontrol to itself?)
UPDATE #2
I've added a GoogleDrive link to the project here if anyone wants to try the solution
Thanks
Combobox is not suitable control for multiselection, because it has given behaviour, that when yo select item, Combobox closes itself. That's why Combobox doest not have SelectionMode property like ListBox. I think that ListBox inside expander is what you need.
Generic Types are not a way to go. WPF handles this different, better way. Take listbox as an example. If you bind listbox.itemssource to generic observable collection, and you try to define e.g ItemTemplate, you get full intellisense when writing bindings and warning if you bind to not existing property. http://visualstudiomagazine.com/articles/2014/03/01/~/media/ECG/visualstudiomagazine/Images/2014/03/Figure8.ashx WPF designer automatically recognizes type parameter of your observable collection. Of cousre you need to specify type of datacontext in your page by using something like this: d:DataContext="{d:DesignInstance search:AdvancedSearchPageViewModel}". However your control dont have to be and shouldn't be aware of type of items.
Following example demonstrates control that meets your requirements:
<Expander>
<Expander.Header>
<ItemsControl ItemsSource="{Binding ElementName=PART_ListBox, Path=SelectedItems}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock>
<Run Text="{Binding Mode=OneWay}" />
<Run Text=";" />
</TextBlock>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Expander.Header>
<Expander.Content>
<ListBox x:Name="PART_ListBox" SelectionMode="Multiple">
<ListBox.ItemsSource>
<x:Array Type="system:String">
<system:String>ABC</system:String>
<system:String>DEF</system:String>
<system:String>GHI</system:String>
<system:String>JKL</system:String>
</x:Array>
</ListBox.ItemsSource>
</ListBox>
</Expander.Content>
</Expander>
I reccomend you to create control derived from ListBox (not usercontrol).
I have hardcoded datatemplates, but you should expose them in your custom dependency properties and use TemplateBinding in you control template. Of course you need to modify expander so it looks like combobox and ListBoxItem style so it looks like CheckBox, but it is ease.

How can I insert a "Select All" item at the top of a ComboBox with its ItemsSource set?

I have a ComboBox with its ItemsSource set to an IList of MyClass objects. I overrode the ComboBox's ItemTemplate to display a CheckBox next to the items. I want to have an item at the top that says "Select All" and when the user checks that CheckBox, the code checks all CheckBoxes. My question is, what is the MVVM way of doing this?
I don't want to add a separate MyClass object to the IList. That seems like it would involve too much coupling of the view and the model. Is there a way to add an item directly in the XAML code and give it a Command that checks all of the Checkboxes?
My ComboBox XAML right now is:
<ComboBox ItemsSource="{Binding MyList}" Width="200">
<ComboBox.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding Name}" IsChecked="{Binding Selected}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
That looks like:
I'd like it to look like:
My MyClass is simply this:
public class MyClass
{
public string Name { get; set; }
public bool Selected { get; set; }
}
Edit: I found a way to add an item to the collection in the XAML code using the example here. I would still need a way run code when the user checks the checkbox for that "Select All" item. To just add the item, the code is this:
<ComboBox Width="200">
<ComboBox.Resources>
<CollectionViewSource x:Key="comboBoxSource" Source="{Binding Path=MyList}" />
</ComboBox.Resources>
<ComboBox.ItemsSource>
<CompositeCollection>
<local:MyClass Name="Select All" Selected="False">
</local:MyClass>
<CollectionContainer Collection="{Binding Source={StaticResource comboBoxSource}}" />
</CompositeCollection>
</ComboBox.ItemsSource>
<ComboBox.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding Name}" IsChecked="{Binding Selected}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
Id personally just modify the CheckBox's Template and add custom CheckBox there with Click handler, nothing too fancy, easy to understand.
http://msdn.microsoft.com/en-us/library/ms752094(v=vs.110).aspx
From there you can modify this part:
<ScrollViewer Margin="4,6,4,6"
SnapsToDevicePixels="True">
<StackPanel IsItemsHost="True"
KeyboardNavigation.DirectionalNavigation="Contained" />
</ScrollViewer>
The other way I would model is this simply as to create
public class MyClassViewModel
{
public string Name { get; set; }
public bool Selected { get; set; }
public ICommand Execute {get; set;}
}
and add custom object to your IList. This will work nicely, without any crap, your viewmodel has no idea about view, + it's testable. win-win for everyone.
Whenever your Button to select all is pressed you can invoke action, binding to itemssource, enumerating throughout collection setting Selected value to true. This solutiond requires Blend interface.
<local:MyClass Name="Select All" Selected="False">
<i:Interaction.Triggers>
<ic:DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=Selected}" Value="True">
<local:SelectAll TargetObject="{Binding RelativeSource={RelativeSource AncestorType=ComboBox}, Path=ItemsSource}"/>
</ic:DataTrigger>
</i:Interaction.Triggers>
</local:MyClass>
and then create class SelectAll as follows
public class SelectAll : TargetedTriggerAction<List<MyClass>>
{
protected override void Invoke(object parameter)
{
if (Target is List<MyClass>)
foreach (var elem in (List<MyClass>)Target)
elem.Selected = true;
}
}
You have to registed the event when selecting a item, and when selecting the item,check that the item selected is the first one(using index) and then,change the selection to what do you need.
(Dont have the IDE here,sorry for not code).
I figured out a way to do it that uses some simpler concepts.
My first problem was that I wanted to add a "Select All" item to the top of the list without actually adding an item to the databound IList<MyClass>. I edited my original post to show that I can use a CompositeCollection to do this. I made the "Select All" databound object a subclass of MyClass and called the class MyClassSelectAll. More on this below.
My second problem was that I needed to handle the CheckBox.Click event differently for the "Select All" item than for the regular MyClass items. I gave each CheckBox a Command that I can use to check the CheckBoxes. To do this, I need a reference to both the IList<MyClass> and the CheckBox on which the user clicked (more specfically the MyClass object databound to that CheckBox).
I found one way to solve my second problem is to set the CheckBox.CommandParameter to a MultiBinding that contains those two objects. In the Command.Execute(object) method, I can easily check if the current MyClass is a MyClassSelectAll, and if so, I can loop through the IList<MyClass> list (which doesn't contain the "Select All" item) and set the "Selected" property appropriately.
Here's my relevant XAML code:
<Grid.Resources>
<local:MyCommand x:Key="kMyCommand" />
<local:MyConverter x:Key="kConv" />
</Grid.Resources>
<ComboBox Width="200">
<ComboBox.Resources>
<CollectionViewSource x:Key="comboBoxSource" Source="{Binding Path=MyList}" />
</ComboBox.Resources>
<ComboBox.ItemsSource>
<CompositeCollection>
<local:MyClassSelectAll Name="Select All" Selected="False" />
<CollectionContainer Collection="{Binding
Source={StaticResource comboBoxSource}}" />
</CompositeCollection>
</ComboBox.ItemsSource>
<ComboBox.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding Name}"
IsChecked="{Binding Selected}"
Command="{StaticResource kMyCommand}">
<CheckBox.CommandParameter>
<MultiBinding Converter="{StaticResource kConv}">
<Binding RelativeSource="{RelativeSource
FindAncestor, AncestorType={x:Type Window}}"
Path="DataContext.MyList" />
<Binding Path="." />
</MultiBinding>
</CheckBox.CommandParameter>
</CheckBox>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
I updated MyClass to implement INotifyPropertyChanged appropriately. The code is simple, so I won't post it here.
MyConverter is pretty simple as well; it just packages the list and current item object into a MyCommandArgs object:
class MyConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
MyCommandArgs result = new MyCommandArgs()
{
MyList = values[0] as IList<MyClass>,
CurrentItem = values[1] as MyClass
};
return result;
}
public object[] ConvertBack(object value, Type[] targetTypes,
object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
The MyCommandArgs class that I use to pass around the MyList and CurrentItem references is a simple container class so I won't post it here.
MyClassSelectAll is the simplest; its only job is to flag the "Select All" MyClass object as something that selects all.
class MyClassSelectAll : MyClass
{
}
Finally, The MyCommand object that handles the click event.
class MyCommand : ICommand
{
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
var mylist = parameter as MyCommandArgs;
if (mylist != null && mylist.CurrentItem is MyClassSelectAll)
{
foreach (var item in mylist.MyList)
{
item.Selected = mylist.CurrentItem.Selected;
}
}
}
}
Thanks everyone. I was really stuck on this. It seems like many times when I have a problem doing something in WPF, the solution is to use a Converter.

ListBox SelectionChanged not getting called

The below is my code for List box with button.
I have two issues here:
1) SelectionChanged not firing for me to get the selected item and its value.
2) My list box is for multiple items selection, so when i select one item the background is not set on the button.
How to solve these issues ?
<ListBox Name="listBox"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
SelectionChanged="TopicListboxSelectionChanged"
ScrollViewer.VerticalScrollBarVisibility="Disabled">
<ListBox.ItemTemplate>
<DataTemplate>
<Button Name="AnswerCell"
Width="456"
Content="{Binding Path=Value}"
Background="#FFF2F4F7"
Foreground="Black"
Style="{StaticResource CellStyle}">
<Button.ContentTemplate>
<DataTemplate>
<TextBlock
Style="{StaticResource TextStyle}"
Padding="0,20,0,20"
TextAlignment="Center"
Text="{Binding}"/>
</DataTemplate>
</Button.ContentTemplate>
</Button>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
EDIT
Here my text block with border
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Name="AnswerCellBack" Margin="0,0,0,4" Orientation="Horizontal">
<Border Name="borderColor" Background="#FFF2F4F7">
<TextBlock Name="Answertext"
Width="456"
Padding="10,20,10,20"
TextAlignment="Center"
Text="{Binding Path=AnswerValue}"
Style="{StaticResource AnswerTextStyle}"/>
</Border>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
Issues here:
1) How to change the selection item background color, i have set Border background in XAML.
2) How to add Multiple item selection.
I believe that your problem is your DataTemplate which consists of a button. A button will normaly handled the routed mouseclick event and will not give the Listbox an opertunity to act on it, therefore your not getting a selection event.
Try to change your button element to a border for example and see if your event is fireing then?
It might also not be a bad idea to use an attached property and bind the selection change to a command,
public class SelectionChangeCommand : DependencyObject
{
public static bool GetIsRegistered(DependencyObject obj)
{
return (bool)obj.GetValue(IsRegisteredProperty);
}
public static void SetIsRegistered(DependencyObject obj, bool value)
{
obj.SetValue(IsRegisteredProperty, value);
}
// Using a DependencyProperty as the backing store for IsRegistered. This enables animation, styling, binding, etc...
public static readonly DependencyProperty IsRegisteredProperty =
DependencyProperty.RegisterAttached("IsRegistered", typeof(bool), typeof(SelectionChangeCommand), new PropertyMetadata(false, new PropertyChangedCallback(RegisterForCommand)));
private static void RegisterForCommand(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is Selector)
{
Selector sel = (Selector)d;
if ((bool)e.NewValue)
{
sel.SelectionChanged += sel_SelectionChanged;
}
else
{
sel.SelectionChanged -= sel_SelectionChanged;
}
}
}
static void sel_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (sender is Selector)
{
Selector sel = (Selector)sender;
ICommand command = GetCommand(sel);
if (command!=null && command.CanExecute(null))
command.Execute(sel);
}
}
public static ICommand GetCommand(DependencyObject obj)
{
return (ICommand)obj.GetValue(CommandProperty);
}
public static void SetCommand(DependencyObject obj, ICommand value)
{
obj.SetValue(CommandProperty, value);
}
// Using a DependencyProperty as the backing store for Command. This enables animation, styling, binding, etc...
public static readonly DependencyProperty CommandProperty =
DependencyProperty.RegisterAttached("Command", typeof(ICommand), typeof(SelectionChangeCommand), new PropertyMetadata(null));
}
after you reference the xmlns you could use the code in xaml like so:
<ListBox kernelAttached:SelectionChangeCommand.Command="{Binding SelectedLinkCommand}"
kernelAttached:SelectionChangeCommand.IsRegistered="True" >

Categories

Resources