DataTrigger does not reevaluate - c#

I have a ListBox and I want to change the appearance of each ListBoxItem based on the index of the previous ListBoxItem in the ListBox.
In order to do that I have created a CustomListBoxItem which inherits from ListBoxItem and have set a Style for it.
Setters.Add(new Setter(ListBoxItem.ContentTemplateProperty, new CustomListBoxItemContentTemplate());
In the constructor of CustomListBoxItemContentTemplate class which inherits from DataTemplate I have a DataTrigger as follows:
DataTrigger dt = new DataTrigger();
dt.Binding = new Binding()
{
RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(CustomListBoxItem), 1),
Mode = BindingMode.OneWay,
UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,
Converter = new CustomConverter()
};
dt.Value = true;
Triggers.Add(dt);
And finally in the Convert method of the CustomConverter class I have implemented the IValueConverter as follows:
var item = value as CustomBoxItem;
if (item == null)
return false;
/// I get the ItemsControl here
ItemsControl ic = ItemsControl.ItemsControlFromItemContainer(item);
/// and the index of the CustomListBoxItem
int index = ic.ItemContainerGenerator.IndexFromContainer(item);
if (index == 0)
return true;
var previousItem = ic.ItemContainerGenerator.ContainerFromIndex(index - 1) as CustomListBoxItem;
/// ....
I should say every thing works perfectly fine in the first place when the ItemsSource is bound to the source collection but when I add or remove items to the CustomListBox, the DataTrigger does not reevaluate the whole items. it does only for the added items.
I really don't know what the problem is, or maybe my approach is not very good to do the job.
Any help is appreciated. Thanks a lot.

Related

View Model Items is Null after updating ItemsSource in User Control

I'm working on a WPF MVVM application and running into an issue. I'm familiar with WPF itself, however I've rarely used MVVM and I suspect I am doing something that MVVM doesn't support, however I don't know how else to accomplish what I am trying to do.
In the application, I have a user control called Agenda. It consist several controls including a text box, a button to add a new agenda item, and a list box with a custom template. The template includes an expander where the header is the agenda item title, up/down arrows to reorder items, and a button to delete the item. The expander content contains a toolbar and a rich text box. In the agenda UC I have a dependency property called ItemsSource which is an IEnumerable<AgendaItem>.
Now, I have a view called Appointment, its associated VM (AppointmentViewModel), and its model (AppointmentModel). In the model, there is a field called AgendaItems which is an ObservableCollection<AgendaItem>. The agenda UC is used within the appointment view and the UC's ItemsSource is bound to the Model.AgendaItems (the observable collection).
The problem I'm having is when I try to handle the buttons to reorder the agenda items in the UC. As an example, for the button to move an agenda item up the list, this is the code in the UC:
var tb = sender as Button;
var tag = tb.Tag as AgendaItem;
var lst = ItemsSource.ToList();
var index = lst.IndexOf(tag);
if(index > 0)
{
lst.RemoveAt(index);
lst.Insert(index - 1, tag);
ItemsSource = lst;
}
The tag of the up arrow is bound to the specific agenda item in the list so I know which item is being moved. The problem comes after I update the ItemsSource property doing ItemsSource = lst. After that line executes, the AgendaItems ObservableCollection in the VM is null. The binding mode is set to TwoWay.
Since the the appointment UC is used in various windows in the application, it made sense to me that the reordering of agenda items should be taken care of my the UC instead of duplicating code in every window which makes use of the UC. But updating the ItemsSource property in the UC results in the collection in the VM being null.
For reference, the ItemsSource property in the UC is defined as:
public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register("ItemsSource", typeof(IEnumerable<AgendaItem>), typeof(Agenda), new PropertyMetadata(null, new PropertyChangedCallback(OnItemsSourceChanged)));
There is the regular .NET property:
public IEnumerable<AgendaItem> ItemsSource
{
get => (IEnumerable<AgendaItem>)GetValue(ItemsSourceProperty);
set => SetValue(ItemsSourceProperty, value);
}
And the OnItemsSourceChanged method is:
private static void OnItemsSourceChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
if (obj is Agenda control)
{
if (e.OldValue is INotifyCollectionChanged oldValueINotifyCollectionChanged)
{
oldValueINotifyCollectionChanged.CollectionChanged -= control.ItemsSource_CollectionChanged;
}
if (e.NewValue is INotifyCollectionChanged newValueINotifyCollectionChanged)
{
newValueINotifyCollectionChanged.CollectionChanged += control.ItemsSource_CollectionChanged;
}
}
}
Any help/guidance on how I can reorder the ItemsSource collection in the UC without breaking the VM would be greatly appreciated. Thank you in advance.
Since calling .ToList() on Enumerable creates new list and reassigning it to ItemsSource doesn't work, maybe define the ItemsSourceProperty as IList<AgendaItem> since this is the interface that you need to reorder the items.
public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register("ItemsSource", typeof(IList<AgendaItem>), typeof(Agenda), new PropertyMetadata(null, new PropertyChangedCallback(OnItemsSourceChanged)));
(don't forget to change the signature of the ItemsSource property ).
You could also create list extension method to call it in your code like this:
ItemsSource.Move(ItemsSource.IndexOf(tag), MoveDirection.Up);
public static void Move<T>(this IList<T> list, int iIndexToMove,
MoveDirection direction)
{
if (list.Count > 0 && (direction == MoveDirection.Down && iIndexToMove < list.Count - 1)
|| (direction == MoveDirection.Up && iIndexToMove > 0))
{
if (direction == MoveDirection.Up)
{
var old = list[iIndexToMove - 1];
list[iIndexToMove - 1] = list[iIndexToMove];
list[iIndexToMove] = old;
}
else
{
var old = list[iIndexToMove + 1];
list[iIndexToMove + 1] = list[iIndexToMove];
list[iIndexToMove] = old;
}
}
}
public enum MoveDirection
{
Up,
Down
}
By changing the data type of ItemsSource from IEnumerable<AgendaItem> to ObservableCollection<AgendaItem> resolved the issue. Thank you to everyone who responded. It is much appreciated.

Is it possible to edit a cell with a CheckBox and TextBlock

I have the following coding where I bind a CheckBox and TextBlock into one DataGridTemplateColumn.
Would it be possible for me to edit the cell with the checkbox and textbox when I click on the cell itself to edit the text inside of it? I still want to be able to set my CheckBox to true or false at the same time as editing the text within the textblock.
Here is my coding:
private void btnFeedbackSelectSupplier_Click(object sender, RoutedEventArgs e)
{
DataGridTemplateColumn columnFeedbackSupplier = new DataGridTemplateColumn();
columnFeedbackSupplier.Header = "Supplier";
columnFeedbackSupplier.CanUserReorder = true;
columnFeedbackSupplier.CanUserResize = true;
columnFeedbackSupplier.IsReadOnly = false;
//My stack panel where I will host the two elements
var stackPanel = new FrameworkElementFactory(typeof(StackPanel));
stackPanel.SetValue(StackPanel.OrientationProperty, Orientation.Horizontal);
DataTemplate cellTemplate = new DataTemplate();
//Where I create my checkbox
FrameworkElementFactory factoryCheck = new FrameworkElementFactory(typeof(CheckBox));
Binding bindCheck = new Binding("TrueFalse");
bindCheck.Mode = BindingMode.TwoWay;
factoryCheck.SetValue(CheckBox.IsCheckedProperty, bindCheck);
stackPanel.AppendChild(factoryCheck);
//Where I create my textblock
FrameworkElementFactory factoryText = new FrameworkElementFactory(typeof(TextBlock));
Binding bindText = new Binding("Supplier");
bindText.Mode = BindingMode.TwoWay;
factoryText.SetValue(TextBlock.TextProperty, bindText);
stackPanel.AppendChild(factoryText);
cellTemplate.VisualTree = stackPanel;
columnFeedbackSupplier.CellTemplate = cellTemplate;
DataGridTextColumn columnFeedbackSupplierItem = new DataGridTextColumn();
columnFeedbackSupplier.Header = (cmbFeedbackSelectSupplier.SelectedItem as DisplayItems).Name;
dgFeedbackAddCost.SelectAll();
IList list = dgFeedbackAddCost.SelectedItems as IList;
IEnumerable<ViewQuoteItemList> items = list.Cast<ViewQuoteItemList>();
var collection = (from i in items
let a = new ViewQuoteItemList { Item = i.Item, Supplier = i.Cost, TrueFalse = false }
select a).ToList();
dgFeedbackSelectSupplier.Columns.Add(columnFeedbackSupplier);
dgFeedbackSelectSupplier.ItemsSource = collection;
}
My example of how it looks now and how I would like to edit that R12 value inside the cell, while still being able to set the checkbox to true or false.
As for my original question, YES you can edit the cell with a CheckBox inside, but instead of a TextBlock I used a TextBox and I changed my the following coding from my question:
var stackPanel = new FrameworkElementFactory(typeof(StackPanel));
stackPanel.SetValue(StackPanel.OrientationProperty, Orientation.Horizontal);//Delete this line
To
var dockPanel = new FrameworkElementFactory(typeof(DockPanel));
Because a StackPanel does not have support for certain elements (like a TextBox) to fill the remaining available space, where a DockPanel does have support for it.
And then I added this line to make my TextBox fill the remaining space availble
factoryText.SetValue(TextBox.HorizontalAlignmentProperty, HorizontalAlignment.Stretch);
Hope this will help someone else out there :)
Why do you want to use TextBlock instead of TextBox ? If you want to expand the full width of my column length, then just set HorizontalAlignment to Stretch like that:
FrameworkElementFactory factoryText = new FrameworkElementFactory(typeof(TextBox));
factoryText.Text = HorizontalAlignment.Stretch;
Update:
And put your TextBox into Grid or DockPanel; as Zach Johnson says that StackPanel is meant for "stacking" things even outside the visible region, so it won't allow you to fill remaining space in the stacking dimension.

Filtered Combobox ItemsSource binding issue

What I'm trying to do :
I have 2 comboboxes, a "regular" one, and a filtrable one. On the filtrable combobox ItemsSource is binded to a property of the first combobox SelectedItem.
Here is some XAML to demonstrate the relation (I removed some properties):
<ComboBox Name="cbx_poche" ItemsSource="{Binding DataContext.ListePoches, ElementName=Main}" SelectedItem="{Binding PocheCible}" />
<controls:FilteredComboBox ItemsSource="{Binding SelectedItem.SupportsEligibles, ElementName=cbx_poche}" SelectedItem="{Binding Support}" />
The FilteredComboBox is a derived class from ComboBox inspired by those articles : Building a Filtered ComboBox for WPF / WPF auto-filtering combo.
The user can type in the combobox and it filters the list to display the items matching. The default behavior of the combobox is NOT desired (it automatically completes what the user types), this is why it's derived.
Those comboboxes above are in a ItemsControl element, because I need to have one row for each item in a specific collection. The SelectedItem properties of the comboboxes are binded to the item in this collection.
The result :
The problem :
It works quite well... as long as you don't select the same item in the first combobox (like in the example above : if I type some text that doesn't match the above combo, it will be reset).
As soon as several FilteredComboBox are linked to the same item in the first combobox (so binded to SelectedItem.SupportsEligibles), typing text in the FilteredComboBox filters both lists.
I know why it does that, I don't know how to fix it. So I tried two things :
Code 1 (current code) :
The problem is that the code uses the default view on the list, so all controls binded to this list will apply the same filter :
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{
if (newValue != null)
{
if (ItemsSourceView != null)
ItemsSourceView.Filter -= this.FilterPredicate;
ItemsSourceView = CollectionViewSource.GetDefaultView(newValue);
ItemsSourceView.Filter += this.FilterPredicate;
}
base.OnItemsSourceChanged(oldValue, newValue);
}
Code 2 :
So my (naïve) idea was to get a local view from the binded view. I works well for filtering, but breaks the binding (changing the selected item in the first combo doesn't update the list after the first pass)
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{
if (newValue != null && newValue != ItemsSourceView)
{
if (ItemsSourceView != null)
ItemsSourceView.Filter -= this.FilterPredicate;
ItemsCollectionViewSource = new CollectionViewSource { Source = newValue };
ItemsSourceView = ItemsCollectionViewSource.View;
ItemsSourceView.Filter += this.FilterPredicate;
this.ItemsSource = ItemsSourceView; // Breaks the binding !!
}
base.OnItemsSourceChanged(oldValue, newValue);
}
I'm stuck here.
I'm looking for some event or Binding class that I could use to be notified of the binding change, in order to update the view. Or maybe getting the view applied without having to change the ItemsSource
I ended up using a quite lame workaround, so I'm still interested in smart answers.
For people interested : I added another similar ItemsSource2 Dependency Property (still didn't find a nice name for it) on which I bind my item list, instead of the original ItemsSource.
When this items source is changed, the control gets a new CollectionView (not the default one) and sets it to the "standard" ItemsSource.
The other elements of the control remains identical (similar to the code in the linked articles).
public static readonly DependencyProperty ItemsSource2Property =
DependencyProperty.Register(
"ItemsSource2",
typeof(IEnumerable),
typeof(FilteredComboBox),
new UIPropertyMetadata((IEnumerable)null, new PropertyChangedCallback(OnItemsSource2Changed)));
[Bindable(true)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public IEnumerable ItemsSource2
{
get { return (IEnumerable)GetValue(ItemsSource2Property); }
set
{
if (value == null)
{
ClearValue(ItemsSource2Property);
}
else
{
SetValue(ItemsSource2Property, value);
}
}
}
private static void OnItemsSource2Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var ic = (FilteredComboBox)d;
var oldValue = (IEnumerable)e.OldValue;
var newValue = (IEnumerable)e.NewValue;
if (newValue != null)
{
//Prevents the control to select the first item automatically
ic.IsSynchronizedWithCurrentItem = false;
var viewSource = new CollectionViewSource { Source = newValue };
ic.ItemsSource = viewSource.View;
}
else
{
ic.ItemsSource = null;
}
}

Custom control and element binding

I'm creating custom control which will contain grid with dynamically added (or removed) rows and columns. Each cell has to contain checkbox which will be binded with some model. If user clicks on checkbox, the dictionary should refresh.
Model.cs
public class Model
{
public Model(List<PanelModel> panels)
{
this.Panels = new Dictionary<int, bool>();
panels.ForEach(panel => this.Panels.Add(panel.Id, false));
}
...
public Dictionary<int, bool> Panels { get; set; }
}
CustomControl.cs
CheckBox checkBox = new CheckBox
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
};
Grid.SetRow(checkBox, index);
Grid.SetColumn(checkBox, innerIndex);
Binding binding = new Binding(string.Format("Panels[{0}]", panel.Id));
binding.Source = this.Configurations;
binding.Mode = BindingMode.TwoWay;
checkBox.SetBinding(CheckBox.IsCheckedProperty, binding);
this.mainGrid.Children.Add(checkBox);
How can I bind specific checkbox with element in dictionary Panels? Dictionary contains int - bool pair which means, that panel with ID x is on or off.
Now, if I click on checkbox, nothing happenes. Debugger shows, that all panels are false (off).
From what you describe, I would approach this with an ItemsControl where the ItemsSource is bound to your collection.
Then add an ItemTemplate that presents the bool via a binding to a checkbox.
You can set the ItemsControl PanelTemplate to a WrapPanel to still get the grid layout look.

Binding a DataGrid*Column to data in code-behind

I would like to bind a DataGrid*Column (in this particular case, a DataGridTextBox) to its data in the code-behind. This is because, depending on a CheckBox's IsClicked property, the Column needs to bind to different collections.
Solutions such as this one all point to the following sort of code:
var binding = new Binding("X");
XColumn.Binding = binding;
Now, I've made use of this sort of code in other parts of my program with success, just not with a DataGrid*Column. With the column, however, this is not working as expected, since in fact all the rows of the column are presenting the X-value of the first element of the collection. This is confirmed when I edit any of the cells and all of them are altered, meaning they are all bound to the same single element of the collection, not to the collection as a whole.
Here is the relevant code:
//This is called whenever the CheckBox EqualToResults is clicked
void ControlBindings()
{
//only showing for (.IsChecked == true), but the other case is similar
//and presents the same problems
if (EqualToResults.IsChecked == true)
{
var cable = DataContext as NCable;
//This is the DataGrid object
Coordinates.ItemsSource = cable;
var binding = new Binding("X");
binding.Source = cable.Points;
//XColumn is the DataGridTextColumn
XColumn.Binding = binding;
}
}
Should it be relevant, here's the relevant code for the NCable class.
public class NCable : DependencyObject, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<NPoint> Points;
public static DependencyProperty PointsProperty = DependencyProperty.Register("Points", typeof(ICollectionView), typeof(NCable));
public ICollectionView IPointCollection
{
get { return (ICollectionView)GetValue(PointsProperty); }
set { SetValue(PointsProperty, value); }
}
public NCable(string cableName)
{
Points = new ObservableCollection<NPoint>();
for (int i = 0; i < 11; i++)
Points.Add(new NPoint(1,1));
IPointCollection = CollectionViewSource.GetDefaultView(Points);
}
}
EDIT 13/05: I've seen somewhere that one must also set the ItemsSource of the DataGrid in this case, so I've done that as well (edited the original code), but still to no avail. The entire column is still bound to the first element of the collection.
Figured it out. In this case, the DataGrid.ItemsSource must be defined (as per the edit in the oP), but the binding.Source must be left undefined. Therefore, the functional code-behind is
void ControlBindings()
{
if (EqualToResults.IsChecked == true)
{
var cable = DataContext as NCable;
Coordinates.ItemsSource = cable;
var binding = new Binding("X");
//REMOVE binding.Source = cable.Points;
XColumn.Binding = binding;
}
}

Categories

Resources