I have multiple expanders, and I was looking for a way to collapse all others the expanders when one of them is expanded. And I found this solution here
XAML:
<StackPanel Name="StackPanel1">
<StackPanel.Resources>
<local:ExpanderToBooleanConverter x:Key="ExpanderToBooleanConverter" />
</StackPanel.Resources>
<Expander Header="Expander 1"
IsExpanded="{Binding SelectedExpander, Mode=TwoWay, Converter={StaticResource ExpanderToBooleanConverter}, ConverterParameter=1}">
<TextBlock>Expander 1</TextBlock>
</Expander>
<Expander Header="Expander 2"
IsExpanded="{Binding SelectedExpander, Mode=TwoWay, Converter={StaticResource ExpanderToBooleanConverter}, ConverterParameter=2}">
<TextBlock>Expander 2</TextBlock>
</Expander>
<Expander Header="Expander 3"
IsExpanded="{Binding SelectedExpander, Mode=TwoWay, Converter={StaticResource ExpanderToBooleanConverter}, ConverterParameter=3}">
<TextBlock>Expander 3</TextBlock>
</Expander>
<Expander Header="Expander 4"
IsExpanded="{Binding SelectedExpander, Mode=TwoWay, Converter={StaticResource ExpanderToBooleanConverter}, ConverterParameter=4}">
<TextBlock>Expander 4</TextBlock>
</Expander>
</StackPanel>
Converter:
public class ExpanderToBooleanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (value == parameter);
// I tried thoses too :
return value != null && (value.ToString() == parameter.ToString());
return value != null && (value.ToString().Equals(parameter.ToString()));
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return System.Convert.ToBoolean(value) ? parameter : null;
}
}
ViewModel:
public class ExpanderListViewModel : INotifyPropertyChanged
{
private Object _selectedExpander;
public Object SelectedExpander
{
get { return _selectedExpander; }
set
{
if (_selectedExpander == value)
{
return;
}
_selectedExpander = value;
OnPropertyChanged("SelectedExpander");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Initialization
var viewModel = new ExpanderListViewModel();
StackPanel1.DataContext = viewModel;
viewModel.SelectedExpander = 1;
// I tried this also
viewModel.SelectedExpander = "1";
It's working fine, but now I want to expand one of the expanders at the application startup !
I already tried to put the values (1, 2 or 3) in SelectedExpander property, but none of expanders get expanded by default !
How can I add this possibility to my expanders ?
Consider what would happen if you called UpdateSource on Expander 2 while Expander 1 is selected:
ConvertBack is called for Expander 2 with its current IsExpanded value (false), and returns null.
SelectedExpander is updated to null.
Convert is called for all other expanders, because SelectedExpander changed, causing all the other IsExpanded values to be set to false as well.
This isn't the correct behavior, of course. So the solution is dependent on the source never being updated except for when a user actually toggles an expander.
Thus, I suspect the problem is that the initialization of the controls is somehow triggering a source update. Even if Expander 1 was correctly initialized as expanded, it would be reset when the bindings were refreshed on any of the other expanders.
To make ConvertBack correct, it would need to be aware of the other expanders: It should only return null if all of them are collapsed. I don't see a clean way of handling this from within a converter, though. Perhaps the best solution then would be to use a one-way binding (no ConvertBack) and handle the Expanded and Collapsed events this way or similar (where _expanders is a list of all of the expander controls):
private void OnExpanderIsExpandedChanged(object sender, RoutedEventArgs e) {
var selectedExpander = _expanders.FirstOrDefault(e => e.IsExpanded);
if (selectedExpander == null) {
viewmodel.SelectedExpander = null;
} else {
viewmodel.SelectedExpander = selectedExpander.Tag;
}
}
In this case I'm using Tag for the identifier used in the viewmodel.
EDIT:
To solve it in a more "MVVM" way, you could have a collection of viewmodels for each expander, with an individual property to bind IsExpanded to:
public class ExpanderViewModel {
public bool IsSelected { get; set; }
// todo INotifyPropertyChanged etc.
}
Store the collection in ExpanderListViewModel and add PropertyChanged handlers for each one at initialization:
// in ExpanderListViewModel
foreach (var expanderViewModel in Expanders) {
expanderViewModel.PropertyChanged += Expander_PropertyChanged;
}
...
private void Expander_PropertyChanged(object sender, PropertyChangedEventArgs e) {
var thisExpander = (ExpanderViewModel)sender;
if (e.PropertyName == "IsSelected") {
if (thisExpander.IsSelected) {
foreach (var otherExpander in Expanders.Except(new[] {thisExpander})) {
otherExpander.IsSelected = false;
}
}
}
}
Then bind each expander to a different item of the Expanders collection:
<Expander Header="Expander 1" IsExpanded="{Binding Expanders[0].IsSelected}">
<TextBlock>Expander 1</TextBlock>
</Expander>
<Expander Header="Expander 2" IsExpanded="{Binding Expanders[1].IsSelected}">
<TextBlock>Expander 2</TextBlock>
</Expander>
(You may also want to look into defining a custom ItemsControl to dynamically generate the Expanders based on the collection.)
In this case the SelectedExpander property would no longer be needed, but it could be implemented this way:
private ExpanderViewModel _selectedExpander;
public ExpanderViewModel SelectedExpander
{
get { return _selectedExpander; }
set
{
if (_selectedExpander == value)
{
return;
}
// deselect old expander
if (_selectedExpander != null) {
_selectedExpander.IsSelected = false;
}
_selectedExpander = value;
// select new expander
if (_selectedExpander != null) {
_selectedExpander.IsSelected = true;
}
OnPropertyChanged("SelectedExpander");
}
}
And update the above PropertyChanged handler as:
if (thisExpander.IsSelected) {
...
SelectedExpander = thisExpander;
} else {
SelectedExpander = null;
}
So now these two lines would be equivalent ways of initializing the first expander:
viewModel.SelectedExpander = viewModel.Expanders[0];
viewModel.Expanders[0].IsSelected = true;
Change the Convert method (given here) content as follows
if (value == null)
return false;
return (value.ToString() == parameter.ToString());
Previous content not working because of object comparison with == operator.
I created an WPF project with just your code, having the StackPanel as the content of the MainWindow and invoking your Initialization code after calling InitializeComponent() in MainWindow() and works like a charm by simply removing
return (value == parameter);
from your ExpanderToBooleanConverter.Convert. Actually #Boopesh answer works too. Even if you do
return ((string)value == (string)parameter);
it works, but in that case only string values are supported for SelectedExpander.
I'd suggest you to try again those other returns in your Convert and if it doesn't work, your problem may be in your initialization code. It is possible that you are setting SelectedExpander before the components have been properly initialized.
I have wrote an example code which demonstrate how to achive what you want.
<ItemsControl ItemsSource="{Binding Path=Items}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<RadioButton GroupName="group">
<RadioButton.Template>
<ControlTemplate>
<Expander Header="{Binding Path=Header}" Content="{Binding Path=Content}"
IsExpanded="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=IsChecked}" />
</ControlTemplate>
</RadioButton.Template>
</RadioButton>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
The model looks like so:
public class Model
{
public string Header { get; set; }
public string Content { get; set; }
}
And the ViewModel expose the model to the view:
public IList<Model> Items
{
get
{
IList<Model> items = new List<Model>();
items.Add(new Model() { Header = "Header 1", Content = "Header 1 content" });
items.Add(new Model() { Header = "Header 2", Content = "Header 2 content" });
items.Add(new Model() { Header = "Header 3", Content = "Header 3 content" });
return items;
}
}
If you dont wont to create a view model (Maybe this is a static) you can use the x:Array markup extension.
you can find example here
You need to set the property after the view is Loaded
XAML
<Window x:Class="UniformWindow.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local ="clr-namespace:UniformWindow"
Title="MainWindow" Loaded="Window_Loaded">
<!- your XAMLSnipped goes here->
</Window>
Codebehind
public partial class MainWindow : Window
{
ExpanderListViewModel vm = new ExpanderListViewModel();
public MainWindow()
{
InitializeComponent();
StackPanel1.DataContext = vm;
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
vm.SelectedExpander = "2";
}
}
IValueConverter
public class ExpanderToBooleanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
// to prevent NullRef
if (value == null || parameter == null)
return false;
var sValue = value.ToString();
var sparam = parameter.ToString();
return (sValue == sparam);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (System.Convert.ToBoolean(value)) return parameter;
return null;
}
}
I did it like this
<StackPanel Name="StackPanel1">
<Expander Header="Expander 1" Expanded="Expander_Expanded">
<TextBlock>Expander 1</TextBlock>
</Expander>
<Expander Header="Expander 2" Expanded="Expander_Expanded">
<TextBlock>Expander 2</TextBlock>
</Expander>
<Expander Header="Expander 3" Expanded="Expander_Expanded" >
<TextBlock>Expander 3</TextBlock>
</Expander>
<Expander Header="Expander 4" Expanded="Expander_Expanded" >
<TextBlock>Expander 4</TextBlock>
</Expander>
</StackPanel>
private void Expander_Expanded(object sender, RoutedEventArgs e)
{
foreach (Expander exp in StackPanel1.Children)
{
if (exp != sender)
{
exp.IsExpanded = false;
}
}
}
Related
I create multiple checkbox using ItemsControl in my WPF. But I need to make a limit by 20 for checkbox that can be checked/ticked by user. How do I can check the checked checkbox?
I tried to research this as much as I can, and even binding checkbox to multiple command, but none of it is working. Below is my code to get through the checkbox that were inside the Itemscontrol. after, IsChecked.
for (int i = 0; i < ItemsControlUnitPerStrip.Items.Count; i++)
{
ContentPresenter container = (ContentPresenter)ItemsControlUnitPerStrip.ItemContainerGenerator.ContainerFromItem(ItemsControlUnitPerStrip.Items[i]);
CheckBox checkBoxChecked = container.ContentTemplate.FindName("CheckBoxUnitPerStrip", container) as CheckBox;
if (checkBoxChecked.IsChecked == true)
{
//iOPC.WriteTag(checkBoxChecked.Uid, checkBoxChecked.IsChecked);
}
}
My XAML code
<GroupBox x:Name="GroupBoxSamplingModeStrip" Header="Unit Per Strip" Grid.Row="0" Grid.Column="1">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsControl x:Name="ItemsControlUnitPerStrip"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Rows="{Binding StripRowsCount}"
Columns="{Binding StripColumnsCount}"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<CheckBox x:Name="CheckBoxUnitPerStrip"
Uid="{Binding Tag}">
<CheckBox.ToolTip>
<ToolTip x:Name="TootlTipUnitPerStrip">
<TextBlock Text="{Binding Key}"/>
</ToolTip>
</CheckBox.ToolTip>
</CheckBox>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</GroupBox>
Here the function code on how I generate the checkbox
private void initializeUnitPerStrip()
{
unitPerStrip = new List<UtilitiesModel>();
int totalRow = samplingModeModel.StripRows = 7;
int totalCol = samplingModeModel.StripColumn = 15;
int frontOffset = 8;
int behindOffset = 0;
for (int c = 1; c < totalCol; c++)
{
for (int r = 1; r < totalRow; r++)
{
unitPerStrip.Add(new UtilitiesModel
{
Key = $"[{c}, {r}]",
Tag = $"{UTAC_Tags.S7Connection}DB{406},X{frontOffset}.{behindOffset}"
});
}
}
ItemsControlUnitPerStrip.ItemsSource = unitPerStrip;
}
1) Binding checkbox property with notify property changed events:
public class UtilitiesModel : NotifyBase
{
private bool _IsChecked = false;
...
// Key
// Tag
...
public bool IsChecked
{
get {return _IsChecked;}
set
{
_IsChecked = value;
OnPropertyChanged("IsChecked");
}
}
}
For convenience, the part responsible for events is placed in a separate small class:
public class NotifyBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
XAML changes:
..
<CheckBox x:Name="CheckBoxUnitPerStrip"
Uid="{Binding Tag}"
IsChecked="{Binding IsChecked}">
<CheckBox.ToolTip>
<ToolTip x:Name="TootlTipUnitPerStrip">
<TextBlock Text="{Binding Key}" />
</ToolTip>
</CheckBox.ToolTip>
</CheckBox>
..
2) Next we shall track events of changing state of checkboxes and add a counter for checked checkboxes;
A slight change in function:
private void initializeUnitPerStrip()
{
..
for (int c = 1; c < totalCol; c++)
{
for (int r = 1; r < totalRow; r++)
{
UtilitiesModel item = new UtilitiesModel
{
Key = "[{c}, {r}]",
Tag = "{UTAC_Tags.S7Connection}DB{406},X{frontOffset}.{behindOffset}"
};
item.PropertyChanged += PropertyChangedFunc;
unitPerStrip.Add(item);
}
}
ItemsControlUnitPerStrip.ItemsSource = unitPerStrip;
}
Add func for checking property changed events:
private void PropertyChangedFunc(object sender, PropertyChangedEventArgs e)
{
UtilitiesModel obj = sender as UtilitiesModel;
if(obj==null)return;
if (e.PropertyName == "IsChecked")
{
iCount1 = obj.IsChecked ? iCount1 + 1 : iCount1 - 1;
if (iCount1 > 19) //Block checking
{
obj.IsChecked = false;
}
}
}
Where iCount1 - is a counter checked checkboxes, just declare it anywhere, for example in samplingModeModel
This answer uses MVVM, so the names of your controls in XAML have been removed as they are not needed in MVVM. Your XAML would look like this:
<Button Content="Count CheckBoxes" Command="{Binding CommandCount}"
HorizontalAlignment="Left"/>
<GroupBox Header="Unit Per Strip" Grid.Row="0" Grid.Column="1">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsControl VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling"
ItemsSource="{Binding Path=UnitPerStrip}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Rows="{Binding StripRowsCount}"
Columns="{Binding StripColumnsCount}"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<CheckBox Uid="{Binding Tag}"
IsChecked="{Binding IsChecked}">
<CheckBox.ToolTip>
<ToolTip >
<TextBlock Text="{Binding Key}"/>
</ToolTip>
</CheckBox.ToolTip>
</CheckBox>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</GroupBox>
The only real difference in the XAML is adding the binding to the 'IsChecked' property in the CheckBox, and setting up a binding to a property called 'UnitPerStrip' for the ItemsSource of the ItemsControl.
In the ViewModel then, you need to set up the UnitPerStrip property:
private List<UtilitiesModel> unitPerStrip;
public List<UtilitiesModel> UnitPerStrip
{
get
{
return unitPerStrip;
}
set
{
if (value != unitPerStrip)
{
unitPerStrip = value;
NotifyPropertyChanged("UnitPerStrip");
}
}
}
The UtilitiesModel class needs a new property called IsChecked to keep track of when the CheckBox is checked. That way you don't have to muck about with messy UI code. It can all be done neatly in the back-end data.
public class UtilitiesModel
{
public string Key { get; set; }
public string Tag { get; set; }
public bool IsChecked { get; set; }
}
The code for generating the CheckBoxes doesn't change a lot. You just need to make sure to add in the IsChecked property and then assign the results to the UnitPerStrip property when finished.
private void initializeUnitPerStrip()
{
List<UtilitiesModel> ups = new List<UtilitiesModel>();
int totalRow = samplingModeModel.StripRows = 7;
int totalCol = samplingModeModel.StripColumn = 15;
int frontOffset = 8;
int behindOffset = 0;
for (int c = 1; c < totalCol; c++)
{
for (int r = 1; r < totalRow; r++)
{
ups.Add(new UtilitiesModel
{
Key = $"[{c}, {r}]",
Tag = $"{UTAC_Tags.S7Connection}DB{406},X{frontOffset}.{behindOffset}",
IsChecked = false;
});
}
}
UnitPerStrip = ups;
}
Then the code to check how many CheckBoxes are checked is very straightforward. It only examines the data in the ViewModel and never has to worry about messing with any of the messiness of the UI:
private void Count()
{
int count = 0;
foreach (UtilitiesModel item in UnitPerStrip)
{
if (item.IsChecked) count++;
}
MessageBox.Show(count.ToString());
}
In case you don't like to add a special IsChecked property to your model you could use a IValueConverter.
A property like IsChecked is view related and shouldn't be part of the view model (if you can avoid it). When you change the control that binds to the view model you might also need to change this property or rename it (e.g. IsExpanded etc.).
I recommend to avoid properties that reflect a visual state inside a view model in general. Your view model would become bloated if adding properties like IsVisible, IsPressed, IsToggled. This properties rather belong to a Control.
The converter (or DataTrigger) approach leaves your binding data models unchanged (with data related properties only). To keep the view model clean and free from UI logic like adjusting properties like IsVisible or IsChecked to reflect e.g. reordering of the collection to the view (e.g. insert or sort operations), all UI logic and visual details like enabling or disabling the controls
should be handled by converters and triggers only:
<!-- Decalare the converter and set the MaxCount property -->
<Window.Resources>
<local:ItemCountToBoolenaConverter x:Key="ItemCountToBoolenaConverter"
MaxCount="20" />
</Window.Resources>
<! -- The actual DataTemplate -->
<ItemsControl.ItemTemplate>
<DataTemplate>
<CheckBox x:Name="CheckBoxUnitPerStrip"
IsEnabled="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=ListBoxItem}, Converter={StaticResource ItemCountToBoolenaConverter}}">
<CheckBox.ToolTip>
<ToolTip x:Name="TootlTipUnitPerStrip">
<TextBlock Text="{Binding Key}" />
</ToolTip>
</CheckBox.ToolTip>
</CheckBox>
</DataTemplate>
</ItemsControl.ItemTemplate>
The ItemCountToBoolenaConverter:
[ValueConversion(typeof(ListBoxItem), typeof(bool))]
class ItemCountToBoolenaConverter : IValueConverter
{
public int MaxCount { get; set; }
#region Implementation of IValueConverter
/// <inheritdoc />
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is ListBoxItem itemContainer && TryFindParentElement(itemContainer, out ItemsControl parentItemsControl))
{
return parentItemsControl.Items.IndexOf(itemContainer.Content) < this.MaxCount;
}
return Binding.DoNothing;
}
/// <inheritdoc />
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
#endregion
// Consider to make this an Extension Method for DependencyObject
private bool TryFindVisualParent<TParent>(DependencyObject child, out TParent resultElement) where TParent : DependencyObject
{
resultElement = null;
if (child == null)
return false;
DependencyObject parentElement = VisualTreeHelper.GetParent(child);
if (parentElement is TParent)
{
resultElement = parentElement as TParent;
return true;
}
return TryFindVisualParent(parentElement, out resultElement);
}
}
I have a list of objects as an ObservableCollection<MyObject>. I am already able to display the name property of these objects in a combobox using XAML within a DataGrid.
Now I have another object AnotherObject which has a property that is defined as a list of strings and each item of that list is the name property of MyObject mentioned above.
In the combobox I want to display the MyObject.name property preceeded by a checkbox.
Let's say that there are 30 items in the checkbox and an instance of AnotherObject.names holds three of them.
Now I want select the checkboxes of those items that are equal to the three items in AnotherObject.names.
How can I achieve this?
Some code:
MyObjectViewModel.cs:
public class MyObjectViewModel
{
private MyObject _myObject;
public MyObjectViewModel(MyObject myObject)
{
this._myObject = myObject;
}
public MyObject MyObject
{
get
{
return _myObject;
}
set
{
_myObject = value;
}
}
public string Name
{
get { return _myObject.Name; }
set
{
_myObject.Name = value;
}
}
public override string ToString()
{
return Name;
}
}
AnotherObjectRowViewmodel.cs:
public class AnotherObjectRowViewModel : INotifyPropertyChanged
{
private AnotherObject _anotherObject;
private ObservableCollection<MyObjectViewModel> _myObjects;
public AnotherObjectRowViewModel(AnotherObject anotherObject, ObservableCollection<MyObjectViewModel> myObjects)
{
this._anotherObject = anotherObject;
this._myObjects = myObjects;
}
public AnotherObject AnotherObject
{
get
{
return _anotherObject;
}
set
{
this._anotherObject = value;
}
}
public string Name
{
get { return _anotherObject.Name; }
set { _anotherObject.Name = value; }
}
public ObservableCollection<MyObjectViewModel> MyObjects {
get
{
return this._myObjects;
}
set
{
_myObjects = value;
}
}
event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged
{
add
{
//throw new NotImplementedException();
}
remove
{
//throw new NotImplementedException();
}
}
}
That's what I tried in the XAML file:
<DataGridTemplateColumn x:Name="NamesColumn" Header="Names">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Grid>
<ComboBox Name="Name" DataContext="{Binding}" ItemsSource="{Binding Path=myObjects}" IsEditable="True" IsReadOnly="True"
VerticalAlignment="Center" SelectionChanged="OnDetailParamsSelectionChanged" >
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox x:Name="chbNames" Width="20" VerticalAlignment="Center" Checked="OnChbDetailParamsCheckBoxChecked" Unchecked="OnChbDetailParamsCheckBoxChecked"></CheckBox>
<TextBlock DataContext="{Binding Path=MyObject}" Text="{Binding Path=Name, Converter={StaticResource StringListConverter}}" VerticalAlignment="Center" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
An example:
The combobox holds a list of 30 names (Name1, ..., Name30);
AnotherObject.names is { Name1, Name2, Name4, Name7 };
In the combobox the selected items shall be Name1, Name2, Name4, Name7. All other items shall stay unselected.
Update 2019-01-06:
This means that the Combobox's ItemsSource={Binding} is MyObject but the checked items shall be stored in AnotherObject. This is why I get this exception whenever I tick a checkbox:
System.Windows.Data Error: 40 : BindingExpression path error: 'xxx' property not found on 'object' ''MyObjectViewModel' (HashCode=34649765)'. BindingExpression:Path=xxx.DetailParams; DataItem='MyObjectViewModel' (HashCode=34649765); target element is 'CheckBox' (Name='chbDetailParams'); target property is 'IsChecked' (type 'Nullable`1')
My XAML contains thefollowing code snippet according to the IsItemsSelectedConverter:
<UserControl.Resources>
<ctb:IsItemSelectedConverter x:Key="IsItemSelectedConverter"/>
</UserControl.Resources>
The checkboxes IsChecked property looks like this:
IsChecked="{Binding Path=Names, Mode=TwoWay, Converter={StaticResource IsItemSelectedConverter}}"
but it doesn't work. Debugging this code, the IsItemsSelectedConverter is never used.
Create a Boolean property in the view model of MyObject, i.e. in MyObjectViewModel that will return a value if Name is in the list of names in AnotherObject.
Create an IsItemSelectedConverter and pass in the list of objects from your data model. Implement IValueConverter in a new class and bind the IsChecked property for each checkbox to two things: value should be the current item, and parameter should be the list of items. The converter should determine whether the current item is in the list of items and return the appropriate boolean.
You will have to create an instance of the Converter for the UI to use. I usually define converters in a separate Resource Dictionary that I make globally available for all XAML files.
My IsItemSelectedConverter looks like this:
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is MyObjectViewModel objectViewModel && parameter is AnotherObjectRowViewModel anotherObjectRowViewModel)
{
return anotherObjectRowViewModel.MyObjects.Contains(objectViewModel);
}
return false;
}
On the XAML, my CheckBox code looks like this: <CheckBox IsChecked="{Binding Mode=OneWay, Converter={StaticResource converter}, ConverterParameter={StaticResource viewModel} }" />
Please note: there are a few problems with the code that will prevent it from running as is, but I'm assuming these are artifacts of copying and pasting your actual code.
I am using MVVM.
I have a CompositeCollection consisting of
ComboboxItem with 'Select a vendor' as content
CollectionContainer which is bounded
When I use the ComboBox XAML code directly in my view the SelectedIndex is set to 0 (as expected).
However, when I put the ComboBox XAML code in a Usercontrol and use the control in my view, the SelectedIndex is set to -1.
Any idea how to fix this issue, so that I can use the usercontrol?
All my bindings work.
Note:
when Combobox XAML code is directly in view: the ComboboxConverter sets the Vendor property to null when 'Select a vendor' is selected by the user.
However, when ComboBox XAML code is in a Usercontrol the code does not get in
if (comboboxItem.Content.ToString() == "Select a vendor")
{
//gets here when code is in view <-> code in control
return null;
}
ComboboxConverter
public class ComboboxConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var vendor = value as Vendor;
if (vendor != null)
{
return vendor;
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
var comboboxItem = value as ComboBoxItem;
if (comboboxItem != null)
{
if (comboboxItem.Content.ToString() == "Select a vendor")
{
//gets here when code is in view <-> code in control
return null;
}
return null;
}
var vendor = value as Vendor;
if (vendor != null)
{
return vendor;
}
return null;
}
}
VendorControl
<UserControl x:Class="Tool.Controls.VendorControl"
xmlns:local="clr-namespace:Tool.Controls"
xmlns:System="clr-namespace:System;assembly=mscorlib"
xmlns:objects='clr-namespace:Tool.Objects'
xmlns:converters='clr-namespace:Tool.Converters'
mc:Ignorable="d" >
<UserControl.Resources>
<converters:ComboboxConverter x:Key='ComboboxConverter' />
</UserControl.Resources>
<Grid>
<ComboBox Name='cmbVendor'
SelectedItem='{Binding Vendor, Converter={StaticResource ComboboxConverter}, Mode=TwoWay}'
Grid.Column='1'
IsSynchronizedWithCurrentItem='True'>
<ComboBox.Resources>
<CollectionViewSource x:Key='VendorsCollection'
Source='{Binding Vendors}' />
<DataTemplate DataType='{x:Type objects:Vendor}'>
<StackPanel Orientation='Horizontal'>
<TextBlock Text='{Binding Name}' />
</StackPanel>
</DataTemplate>
</ComboBox.Resources>
<ComboBox.ItemsSource>
<CompositeCollection>
<ComboBoxItem Content='Select a vendor' />
<CollectionContainer Collection='{Binding Source={StaticResource VendorsCollection}}' />
</CompositeCollection>
</ComboBox.ItemsSource>
</ComboBox>
ViewModel
private void OnWindowLoaded()
{
LoadVendors();
}
ObservableCollection<Vendor> _vendors = new ObservableCollection<Vendor>();
public ObservableCollection<Vendor> Vendors
{
get
{
return _vendors;
}
}
private Vendor _vendor;
public Vendor Vendor
{
get
{
return _vendor;
}
set
{
if (value != _vendor)
{
_vendor = value;
RaisePropertyChanged(nameof(Vendor));
}
}
}
private void LoadVendors()
{
var dVendors = VendorHelper.GetVendors();
if (Vendors.Count > 0)
{
DispatcherHelper.CheckBeginInvokeOnUI(() => Vendors.Clear());
}
dVendors.ForEach(dV =>
{
var vendor = new Vendor(dV);
DispatcherHelper.CheckBeginInvokeOnUI(() => Vendors.Add(vendor));
});
}
At the beginning when your Vendor is null, function Convert will return null as well but your combobox does not know what null means, that's why it sets its selected index value to -1.
You can solve this by creating a behavior which will always select index 0 when it is set to -1.
public class SelectFirstItemBehavior : Behavior<ComboBox>
{
protected override void OnAttached()
{
AssociatedObject.SelectionChanged += AssociatedObject_SelectionChanged;
}
protected override void OnDetaching()
{
AssociatedObject.SelectionChanged -= AssociatedObject_SelectionChanged;
base.OnDetaching();
}
private void AssociatedObject_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var combobox = sender as ComboBox;
if (combobox != null && combobox.SelectedIndex == -1)
{
combobox.SelectedIndex = 0;
}
}
}
And then in your XAML part:
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
...
inside your Combobox:
<i:Interaction.Behaviors>
<SelectFirstItemBehavior/>
</i:Interaction.Behaviors>
i am making a simple list with few options.
This is how it looks like:
Buttons appear when hit the listviewitem and disappear when leave
When press Play, then this button stays Visible and Content changes to Stop
My problems is:
When i press Stop, then this Button stays Visible and Triggers disappear :/
What else i want to do, but i can't is:
When i press Play then Slider appears, otherwise Collapsed
I hope that someone can help me.
My code looks like this so far:
XAML:
<ListView Name="lst">
<ListView.View>
<GridView>
<GridViewColumn>
<GridViewColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Button Name="btnDownload" Content="Download" Visibility="Hidden" MinWidth="100"/>
<Button Name="btnPlay"
Click="btnPlay_Click"
Content="Play"
Visibility="Hidden"
MinWidth="100"/>
</StackPanel>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding
RelativeSource={RelativeSource
Mode=FindAncestor,
AncestorType={x:Type ListViewItem}},
Path=IsMouseOver}"
Value="True">
<Setter TargetName="btnDownload"
Property="Visibility"
Value="Visible"/>
<Setter TargetName="btnPlay"
Property="Visibility"
Value="Visible"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn>
<GridViewColumn.Header>
<GridViewColumnHeader Tag="Name">Name</GridViewColumnHeader>
</GridViewColumn.Header>
<GridViewColumn.CellTemplate>
<DataTemplate>
<StackPanel MinWidth="200">
<TextBlock Text="{Binding Name}"/>
<Slider Name="Slider" Visibility="Visible"/>
</StackPanel>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
CS:
public partial class MainWindow : Window
{
public ListCollectionView MyCollectionView { get; set; }
public ObservableCollection<Songs> songs = new ObservableCollection<Songs>();
public MainWindow()
{
InitializeComponent();
MyCollectionView = new ListCollectionView(songs);
lst.ItemsSource = MyCollectionView;
songs.Add(new Songs(){Name = "Eminem - Superman"});
songs.Add(new Songs(){Name = "Rihanna - Please don't stop the music"});
songs.Add(new Songs(){Name = "Linkin Park - Numb"});
}
private void btnPlay_Click(object sender, RoutedEventArgs e)
{
//Reset all songs
List<Button> buttons = FindVisualChildren<Button>(lst).ToList();
foreach (Button button in buttons)
{
button.Content = "Play";
//Loosing Triggers
}
//Play current
Button btn = sender as Button;
btn.Visibility = Visibility.Visible;
btn.Content = "Stop";
}
private IEnumerable<T> FindVisualChildren<T>(DependencyObject obj) where T : DependencyObject
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(obj, i);
if (child != null && child is T)
{
yield return (T)child;
}
else
{
var childOfChild = FindVisualChildren<T>(child);
if (childOfChild != null)
{
foreach (var subchild in childOfChild)
{
yield return subchild;
}
}
}
}
}
}
public class Songs : INotifyPropertyChanged
{
private string name;
public string Name
{
get { return name; }
set
{
if (name != value)
{
name = value;
NotifyPropertyChanged("Name");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
Project:
MusicList.sln
And ofcourse - sorry for my bad english :)
You could create a value converter that would take the text of your button and return a visibility value based on the text value. Something like this;
public class StringToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var buttonText = (string) value;
switch (buttonText.ToLower())
{
case "stop":
return Visibility.Visible;
default:
return Visibility.Collapsed;
}
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
Create an instance of this class in your xaml by creating a static resource and adding it into your resource dictionary e.g. something like this;
<Window.Resources>
<myNamespace:StringToVisibilityConverter x:Key="StringToVisibilityConverter"/>
</Window.Resources>
Then bind your slider visibility to your button text;
<Slider Name="Slider" Visibility="{Binding ElementName=btnPlay, Path=Content, Converter={StaticResource StringToVisibilityConverter}}"/>
I have a TabControl
I followed the answer chosen here to the letter. The thing is that in my case the ExistingTabs is not an ObservableCollection, but the property of an ObservableCollection:
Public Class TestData : INotifyPropertyChanged // and the required event handler is there also, not shown here
{
public TabItem tiTestTab {get; set;}
// another dozen properties
}
and
public class ReportData
{
public static ObservableCollection<TestData> testData {get;set;}
// another dozen properties
}
Here's what I did:
<Window.Resources>
<CollectionViewSource x:Key="ExistingTabs" Source="{Binding Path=(local:ReportDataSet.testData), Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</Window.Resources>
<TabControl>
<TabControl.ItemsSource>
<CompositeCollection>
<TabItem>SomeSpecialItem</TabItem>
<CollectionContainer Collection="{Binding Source={StaticResource ExistingTabs}}"/>
</CompositeCollection>
</TabControl.ItemsSource>
</TabControl>
This, of course, places the testData in the tabs and not the tiTestTab property.
I am at a loss.
Preferably, XAML-only.
Using C# and Visual Studio 2013.
Thanks.
Xaml code:
<Window.Resources>
<local:CollectionConverter x:Key="collectionConverter" />
<CollectionViewSource x:Key="ExistingTabs" Source="{Binding Path=(local:ReportDataSet.testData), Converter={StaticResource collectionConverter}, UpdateSourceTrigger=PropertyChanged}"/>
</Window.Resources>
<TabControl>
<TabControl.ItemsSource>
<CompositeCollection>
<TabItem Header="test">
<StackPanel>
<Button Content="Add new item" Click="AddNewTabItem"></Button>
<Button Content="Remove last item" Click="RemoveLastItem"></Button>
</StackPanel>
</TabItem>
<CollectionContainer Collection="{Binding Source={StaticResource ExistingTabs}}" >
</CollectionContainer>
</CompositeCollection>
</TabControl.ItemsSource>
</TabControl>
Converter:
public class CollectionConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is ObservableCollection<TestData>)
{
return new ObservableCollection<TabItem>(((ObservableCollection<TestData>)value).
Select(q => q.tiTestTab));
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
ReportDataSet:
public class ReportDataSet
{
public static ObservableCollection<TestData> testData { get; set; }
static ReportDataSet()
{
testData = new ObservableCollection<TestData>();
testData.Add(new TestData()
{
tiTestTab = new TabItem()
{
Header = "test 1"
}
});
testData.CollectionChanged += (s, e) => { OnStaticPropertyChanged("testData"); };
}
public static event EventHandler<PropertyChangedEventArgs> StaticPropertyChanged;
protected static void OnStaticPropertyChanged(string propertyName)
{
var handler = StaticPropertyChanged;
if (handler != null) handler(null, new PropertyChangedEventArgs(propertyName));
}
}
Code-behind (for test purposes):
private void AddNewTabItem(object sender, RoutedEventArgs e)
{
ReportDataSet.testData.Add(new TestData()
{
tiTestTab = new TabItem()
{
Header = "test " + (ReportDataSet.testData.Count + 1)
}
});
}
private void RemoveLastItem(object sender, RoutedEventArgs e)
{
if (ReportDataSet.testData.Count == 0) return;
ReportDataSet.testData.Remove(ReportDataSet.testData.Last());
}
Hope, it helps