Add an item to ListBox and get the ListBoxItem - c#

UPDATE:
I uploaded a demo trying to explain my problem clear. Download it here.
I'm developing an manager class that deals with the selection of ListBox. (The default selection function provided by ListBox cannot satisfy me requirements)
So, when an item is added to the ListBox, my manager class should get the corresponding ListBoxItem and make it selected or unselected.
Although I think ItemContainerGenerator.ItemsChanged should tell some information of the newly added item, it provides same event arg when ListBox.Items.Add is called multiple times(with different parameter), which confuses me a lot. Can anyone tell me how to get the newly generated ListBoxItem for the newly added item.
Code to demonstrate the problem:
<Window x:Class="WpfApplication2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:System="clr-namespace:System;assembly=mscorlib"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
<Button Content="Add two items" Click="Button_Click_1"/>
<ListBox Name="listBox">
<System:Int32>1</System:Int32>
<System:Int32>2</System:Int32>
<System:Int32>3</System:Int32>
</ListBox>
</StackPanel>
</Window>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
SelectionManager selectionManager = new SelectionManager();
selectionManager.Join(listBox);
}
private void Button_Click_1(object sender, RoutedEventArgs e)
{
listBox.Items.Add(4);
listBox.Items.Add(5);
}
}
Here, in Button_Click, two items are added to the listBox, and selectionManager should get the ListBoxItem at same time.
class SelectionManager
{
public void Join(ListBox element)
{
element.ItemContainerGenerator.ItemsChanged += ItemContainerGenerator_ItemsChanged;
}
private List<int> listBoxItemPendingJoinIndexes = new List<int>();
void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
Contract.Requires(listBoxItemPendingJoinIndexes.Count > 0);
ItemContainerGenerator generator = (ItemContainerGenerator)sender;
if (generator.Status != GeneratorStatus.ContainersGenerated)
return;
generator.StatusChanged -= ItemContainerGenerator_StatusChanged;
foreach (var index in listBoxItemPendingJoinIndexes)
{
ListBoxItem listBoxItem = (ListBoxItem)generator.ContainerFromIndex(index);
Join(listBoxItem);
}
listBoxItemPendingJoinIndexes.Clear();
}
void ItemContainerGenerator_ItemsChanged(object sender, ItemsChangedEventArgs e)
{
ItemContainerGenerator generator = (ItemContainerGenerator)sender;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
listBoxItemPendingJoinIndexes.Add(e.Position.Index
+ e.Position.Offset);//same e.Position
generator.StatusChanged += ItemContainerGenerator_StatusChanged;
break;
}
}
}

If your items source is an ObservableCollection, you can use OnCollectionChanged event, which has a NotifyCollectionChangedEventArgs Check NewItems for the list of new items involved in the change.

Maybe create a cutsom listbox class and override the methods?
public class SafeListBox : ListBox
{
delegate void insertDelegate(int i, object o);
public SafeListBox()
{
this.Items = new CustomObjectCollection(this);
}
public new CustomObjectCollection Items
{
get;
set;
}
public class CustomObjectCollection : ListBox.ObjectCollection
{
private ListBox listBox = null;
public CustomObjectCollection(ListBox listBox) : base(listBox)
{
this.listBox = listBox;
}
public new void Insert(int index, object item)
{
if (listBox.InvokeRequired)
{
insertDelegate setTextDel = delegate(int i, object o)
{
base.Insert(i, o);
};
try
{
listBox.Invoke(setTextDel, new object[] { index, item });
}
catch
{
}
}
else
{
base.Insert(index,item);
}
}
}
}

I'm not sure if i fully understand the question. But if it is only about how to manipulate a newly created ListBoxItem once before it is displayed, you could create a derived ListBox and just override the PrepareContainerForItemOverride method.
public class MyListBox : ListBox
{
protected override void PrepareContainerForItemOverride(
DependencyObject element, object item)
{
base.PrepareContainerForItemOverride(element, item);
var listBoxItem = element as ListBoxItem;
...
}
}

Related

WPF DataGrid, ObservableCollection Updates, and Redrawing

I will try to keep this as brief as possible, but there is a fair amount of nuance to this question.
The Workflow
I am working in C# and am using WPF and MVVM for the UI for an addin for Revit (a 3D modeling software from Autodesk).
The overarching goal is to create a window that shows the parameters of a 3D element after it is selected. This is a filtered list that are specific to my organization and our users needs, and allow the user to edit them in order to streamline their workflow. The complication is that because I am working with an API I can only use what tools I am given when interacting with the model.
The issue I am running into lies in the workflow. I have detailed the workflow below.
User Selects a 3D Element
The addin uses the API to pull the parameters and wraps them in a wrapper class and adds them into a custom ObservableCollection to display them in the DataGrid
The user then changes a value in the DataGrid. When the cell loses focus it fires off a command that hooks the API and updates the parameter's value.
The change is made and the internal logic of the element calculates it's values based on the changed parameter
The calculated parameter values are changed in the model
The ViewModel checks each parameter to see if its value has changed, and updates any of the wrapped parameters in the ObservableCollection to reflect the changes.
The ObservableCollection fires off it's collection changed event to notify the DataGrid that values have changed
The DataGrid updates it's values to complete the process.
The issue currently lies in the very last step. Once the collection changed event is complete the wrapped parameter value matches the parameter value from the API, but the DataGrid will not redraw the information. Once you minimize the window, click into the cell, or scroll the DataGrid to where the cell is not visible the cell will show the new value when it comes back into view.
I can't seem to find a way to keep with MVVM principles and force the cells to redraw with their updated value. Am I missing something with this? How do I get the DataGrid to update without having to completely clear and reset the ObservableCollection items?
Things I have Tried
I had to create a custom ObservableCollection to implement INPC for the items in the collection, and from debugging it appears to work as intended. Each time an item in the ObservableCollection is updated it makes the change subscribes it to INPC and raises the collection changed event.
For each of the columns I have the binding set to Mode="TwoWay" and have tried setting the UpdateSourceTrigger="PropertyChanged", and neither helped.
I originally was using a <ContentPresenter/> in a <DataGridTemplteColumn/> to present different cell types, but even using a basic <DataGridTextColumn doesn't work.
---- CODE ----
XAML:
<DataGrid Grid.Row="2" ItemsSource="{Binding TestingParameters, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn IsReadOnly="True" Binding="{Binding Path=Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Header="Name"/>
<DataGridTextColumn IsReadOnly="False" Binding="{Binding Path=ParamValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Header="Param Value">
<i:Interaction.Triggers>
<i:EventTrigger EventName="LostFocus">
<i:InvokeCommandAction Command="{Binding UpdateParametersCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
C# ViewModel
public class ViewModelParameterPane : ViewModelBase
{
private ExternalEvent _event;
private HandlerED _handler;
private UIApplication _uiapp;
private UIDocument _uidoc;
private Document _doc;
private TestObservableCollection<WrappedParameter> _testParameter = new TestObservableCollection<WrappedParameter>();
public TestObservableCollection<WrappedParameter> TestParameter
{
get => _testParameter;
set
{
_testParameter = value;
RaiseProperty(nameof(_testParameter));
}
}
public ViewModelParameterPane(ExternalEvent exEvent, HandlerED handler, UIApplication uiapp)
{
_event = exEvent;
_handler = handler;
_uiapp = uiapp;
_uidoc = _uiapp.ActiveUIDocument;
_doc = _uidoc.Document;
_testParameter.ItemPropertyChanged += _testParameter_ItemPropertyChanged;
_testParameter.CollectionChanged += _testParameter_CollectionChanged;
UpdateParametersCommand = new RelayCommand(CallUpdateParameters);
}
private void _testParameter_ItemPropertyChanged(object sender, ItemPropertyChangedEventArgs<WrappedParameter> e)
{
Debug.WriteLine("PROPERTY CHANGE");
RaiseProperty(nameof(TestParameter));
int index = TestParameter.IndexOf(e.Item);
TestParameter[index] = e.Item;
}
private void _testParameter_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
Debug.WriteLine("COLLECTION CHANGE");
}
private void MakeRequest(RequestIdED request)
{
_handler.Request.Make(request);
_event.Raise();
}
private void CallUpdateParameters() { MakeRequest(RequestIdED.UpdateParameters); }
public void UpdateParameters()
{
Debug.WriteLine("Running Update Parameters");
try
{
using (var transaction = new Transaction(_doc))
{
transaction.Start("T_UpdateParameters");
foreach (WrappedParameter p in TestParameter)
{
string currenValue = p.RevitParameter.AsValueString();
if (p.RevitParameter.AsValueString() != p.ParamValue)
{
bool setValueSuccess = p.SetRevitParameterValue(p.ParamValue);
if(!setValueSuccess)
{
TaskDialog.Show("Parameter Value Not Set", "The parameter value for the parameter " + p.Name + " was not given a valid value and was not changed. Please ensure the units are correct.");
}
}
}
transaction.Commit();
}
}
catch (Exception e)
{
throw new Exception("Something Went Wrong. Check your values.");
}
}
public void UpdateParameterValues()
{
for(var i = 0; i < TestParameter.Count; i++)
{
TestParameter[i].UpdateValues();
}
}
}
C# Parameter Wrapper Class
public class TestParameter : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
internal void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private string _name;
public string Name
{
get => _name;
set
{
_name = value;
OnPropertyChanged(nameof(_name));
}
}
private string _categories;
public string Categories
{
get => _categories;
set
{
_categories = value;
OnPropertyChanged(nameof(_categories));
}
}
private string _paramValue;
public string ParamValue
{
get => _paramValue;
set
{
_paramValue = value;
OnPropertyChanged(nameof(_paramValue));
}
}
private Parameter _revitParameter;
public Parameter RevitParameter
{
get => _revitParameter;
set
{
_revitParameter = value;
OnPropertyChanged(nameof(_revitParameter));
}
}
private ElementId _elementId;
public ElementId ElementId
{
get => _elementId;
set
{
_elementId = value;
OnPropertyChanged(nameof(_elementId));
}
}
public TestParameter(Parameter param)
{
GetRevitParameterValue();
}
public void GetRevitParameterValue()
{
//Get parameter value logic
}
public bool SetRevitParameterValue(string Value)
{
//Set parameter value logic
}
}
C# TestObservableCollection Class
public class TestObservableCollection<T> : ObservableCollection<T>
where T : INotifyPropertyChanged
{
public event EventHandler<ItemPropertyChangedEventArgs<T>> ItemPropertyChanged;
protected override void InsertItem(int index, T item)
{
base.InsertItem(index, item);
item.PropertyChanged += item_PropertyChanged;
}
protected override void RemoveItem(int index)
{
var item = this[index];
base.RemoveItem(index);
item.PropertyChanged -= item_PropertyChanged;
}
protected override void ClearItems()
{
foreach (var item in this)
{
item.PropertyChanged -= item_PropertyChanged;
}
base.ClearItems();
}
protected override void SetItem(int index, T item)
{
var oldItem = this[index];
oldItem.PropertyChanged -= item_PropertyChanged;
base.SetItem(index, item);
item.PropertyChanged += item_PropertyChanged;
}
private void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
OnItemPropertyChanged((T)sender, e.PropertyName);
}
private void OnItemPropertyChanged(T item, string propertyName)
{
var handler = this.ItemPropertyChanged;
if (handler != null)
{
handler(this, new ItemPropertyChangedEventArgs<T>(item, propertyName));
}
}
}
public sealed class ItemPropertyChangedEventArgs<T> : EventArgs
{
private readonly T _item;
private readonly string _propertyName;
public ItemPropertyChangedEventArgs(T item, string propertyName)
{
_item = item;
_propertyName = propertyName;
}
public T Item
{
get { return _item; }
}
public string PropertyName
{
get { return _propertyName; }
}
}

Validation.ErrorEvent is not rised when element with error is removed

I have to show a list where each item will be validated. I am subscribed to Validation.ErrorEvent on a top level to monitor for children.
When I remove item with validation error from list this event is not rised.
In example below I have 3 TextBox on screen, each is bound to int property. Entering wrong value will fire event (Title is changed to "+"), fixing value afterwards will fire event once (Title is changed to "-").
However removing TextBox while having error will not rise event (to clean up) and Title stay "+":
How can I fix that? Ideally I want that this event is automatically rised before removing happens.
Please note: in real project there is complex hierarchy of view models, solutions like "set Title in delete method" would require monitoring for sub-views and propagating that info through all hierarchy, which I'd like to avoid. I'd prefer view-only solution.
MCVE:
public partial class MainWindow : Window
{
public ObservableCollection<VM> Items { get; } = new ObservableCollection<VM> { new VM(), new VM(), new VM() };
public MainWindow()
{
InitializeComponent();
AddHandler(Validation.ErrorEvent, new RoutedEventHandler((s, e) =>
Title = ((ValidationErrorEventArgs)e).Action == ValidationErrorEventAction.Added ? "+" : "-"));
DataContext = this;
}
void Button_Click(object sender, RoutedEventArgs e) => Items.RemoveAt(0);
}
public class VM
{
public int Test { get; set; }
}
xaml:
<StackPanel>
<ItemsControl ItemsSource="{Binding Items}" Height="200">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBox Text="{Binding Test, NotifyOnValidationError=True, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button Content="Remove first" Click="Button_Click" />
</StackPanel>
After a little time, I have a working solution that you could start with. As already mentioned by Ed, You're using the UI as a data structure, which is never a good idea. The MVVM way to do validation is IDataErrorInfo, this is true and really you should be implementing the IDataErrorInfo interface to handle these errors.
On another note, here's what I did to get it working. I am handling the CollectionChanged event for the ObservableCollection of your VM's. When the collection changes, you need to find the element that was actually being removed, if found, we can try and clear it's ValidationError object for that element itself.
Here is the class -
public partial class MainWindow : Window
{
public ObservableCollection<VM> Items { get; } = new ObservableCollection<VM> { new VM(), new VM(), new VM() };
public MainWindow()
{
InitializeComponent();
Items.CollectionChanged += Items_CollectionChanged;
AddHandler(Validation.ErrorEvent, new RoutedEventHandler((s, e) =>
Title = ((ValidationErrorEventArgs)e).Action == ValidationErrorEventAction.Added ? "+" : "-"));
DataContext = this;
}
private void Items_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove) {
foreach (TextBox tb in FindVisualChildren<TextBox>(this))
{
if(tb.DataContext == e.OldItems[0])
{
Validation.ClearInvalid(tb.GetBindingExpression(TextBox.TextProperty));
break;
}
}
}
}
private void Button_Click(object sender, RoutedEventArgs e) => Items.RemoveAt(0);
public static IEnumerable<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject
{
if (depObj != null)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
if (child != null && child is T)
{
yield return (T)child;
}
foreach (T childOfChild in FindVisualChildren<T>(child))
{
yield return childOfChild;
}
}
}
}
}
public class VM
{
public int Test { get; set; }
}
The bread and butter to make this work is Validation.ClearInvalid(tb.GetBindingExpression(TextBox.TextProperty)); which actually removes all ValidationError objects from the BindingExpressionBase object which in this case is TextBox.TextProperty.
Note: There has been no error checking here, you may want to do that.

WPF - DataGrid doesn't show list

My goal is to output a list in a datagrid, but this doesn't work and the datagrid is empty.
I tried to display the list in an other way and it did (but I can't remember what it was) and it worked, except for it not being in a datagrid but just data. I have changed up some things, but back then it reached the end and got displayed.
ViewModel in Mainwindow:
public class ViewModel
{
public List<ssearch> Items { get; set; }
private static ViewModel _instance = new ViewModel();
public static ViewModel Instance { get { return _instance; } }
}
public MainWindow()
{
InitializeComponent();
DataContext = new ViewModel();
//For simplicity, let's say this window opens right away
var Mdata = new MDataWindow { DataContext = DataContext };
Mdata.Show();
}
Other Window for data display:
string searchParam = "status = 1";
public MDataWindow()
{
InitializeComponent();
}
private void AButton_Click(object sender, RoutedEventArgs e)
{
MainWindow.ViewModel.Instance.Items = Search(searchParam);
}
public List<ssearch> Search(string where)
{
{
//Lots of stuff going on here
}
return returnList;
}
And in WPF:
<Window x:Class="WPFClient.MDataWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WPFClient"
mc:Ignorable="d"
Title="MDataWindow" Height="Auto" Width="Auto">
<StackPanel>
<Button x:Name="AButton" Click="AButton_Click" Content="Load" />
<DataGrid ItemsSource="{Binding Items}" />
</StackPanel>
</Window>
I have no clue where the error is and tried to strip the code down as much as possible without killing error sources. The Datagrid just stays empty when I press the "Load" button.
EDIT:
I tried to convert the list into an observableColletion before passing it to the ViewModel, but this didn't work. I am working with a library, which I am not sure how to use observableCollection with, so I converted it instead of using it right away:
VM:
public ObservableCollection<Product> Items { get; set; }
Data Window:
List<Product> pp = Search_Products(searchParam);
var oc = new ObservableCollection<Product>(pp);
MainWindow.ViewModel.Instance.Items = oc;
First, change your List<Product> to an ObservableCollection<Product> as this will help to display the Items of the list on Add/Remove immediately.
This is because ObservableCollection implements the INotifyCollectionChanged interface to notify your target(DataGrid) to which it is bound, to update its UI.
Second, your binding can never work as expected due to changed reference of your collection.
private void AButton_Click(object sender, RoutedEventArgs e)
{
// You are changing your Items' reference completely here, the XAML binding
// in your View is still bound to the old reference, that is why you're seeing nothing.
//MainWindow.ViewModel.Instance.Items = Search(searchParam);
var searchResults = Search(searchParam);
foreach(var searchResult in searchResults)
{
MainWindow.ViewModel.Instance.Items.Add(searchResult);
}
}
Make sure you have changed the List to ObservableCollection upon running the Add loop, else you will get an exception saying the item collection state is inconsistent.
The ViewModel class should implement the INotifyPropertyChanged interface and raise its PropertyChanged event whenever Items is set to a new collection:
public class ViewModel : INotifyPropertyChanged
{
private List<ssearch> _items;
public List<ssearch> Items
{
get { return _items; }
set { _items = value; OnPropertyChanged(); }
}
private static ViewModel _instance = new ViewModel();
public static ViewModel Instance { get { return _instance; } }
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
This is required to notify the view regardless of the type of Items.
If you change the type of Items to ObservableCollection<T>, you should initialize the collection in the view model once:
public class ViewModel
{
public ObservableCollection<ssearch> Items { get; } = new ObservableCollection<ssearch>();
private static ViewModel _instance = new ViewModel();
public static ViewModel Instance { get { return _instance; } }
}
...and then add items to this collection instead of setting the property to a new one:
private void AButton_Click(object sender, RoutedEventArgs e)
{
MainWindow.ViewModel.Instance.Items.Clear();
var search = Search(searchParam);
if (search != null)
foreach (var x in search)
MainWindow.ViewModel.Instance.Items.Add(x);
}

Raise/Fire Listbox SelectionChangedEvent manually

I want to select programatically multiple items in my ListBox. So, to be as mvvm friendly as possible, I create a custom control inherited from ListBox. In this custom control I've made a dependency property allowing items selection changes. Here is the code of the OnPropertyChanged part :
private static void OnSetSelectionToPropertyChanged
(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
InitializableListBox list = d as InitializableListBox;
Dictionary<int, string> toSelect = e.NewValue as Dictionary<int, string>;
if (toSelect == null)
return;
list.SetSelectedItems(toSelect);
}
The selection works great, but this solution does not raise the OnSelectionChanged event
So I try also :
private static void OnSetSelectionToPropertyChanged
(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
InitializableListBox list = d as InitializableListBox;
Dictionary<int, string> toSelect = e.NewValue as Dictionary<int, string>;
if (toSelect == null)
return;
SelectionChangedEventArgs e_selChanged;
List<Object> removed = new List<object>();
List<Object> added = new List<object>();
//Clear the SelectedItems list
while(list.SelectedItems.Count > 0)
{
removed.Add(list.SelectedItems[0]);
list.SelectedItems.RemoveAt(0);
}
//Add each selected items
foreach (var item in toSelect)
{
list.SelectedItems.Add(item);
added.Add(list.SelectedItems[list.SelectedItems.Count - 1]);
}
//Raise the SelectionChanged event
e_selChanged = new SelectionChangedEventArgs(SelectionChangedEvent,removed,added);
list.OnSelectionChanged(e_selChanged);
}
But this was not better. I think I'm not dealing the right way with the event so if you could help me, that would be great.
Thanks in advance.
EDIT
I have found another solution (more a hack actually) than #NETscape. I don't think it's better, but it seems to work pretty well, and it's maybe easier.
The trick is to made a dependency property wich allow you to access the SelectedItems property (wich is readonly and unbindable on the normal ListBox). Here is the code of my custom ListBox :
public class InitializableListBox : ListBox
{
public InitializableListBox()
{
SelectionChanged += CustomSelectionChanged;
}
private void CustomSelectionChanged(object sender, SelectionChangedEventArgs e)
{
InitializableListBox s = sender as InitializableListBox;
if (s == null)
return;
s.CustomSelectedItems = s.SelectedItems;
}
#region CustomSelectedItems DependyProperty
public static DependencyProperty CustomSelectedItemsProperty =
DependencyProperty.RegisterAttached("CustomSelectedItems",
typeof(System.Collections.IList),typeof(InitializableListBox),
new PropertyMetadata(null, OnCustomSelectedItemsPropertyChanged));
public System.Collections.IList CustomSelectedItems
{
get
{
return (System.Collections.IList)GetValue(CustomSelectedItemsProperty);
}
set
{
SetValue(CustomSelectedItemsProperty, value);
}
}
private static void OnCustomSelectedItemsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
InitializableListBox list = d as InitializableListBox;
System.Collections.IList toSelect = e.NewValue as System.Collections.IList;
if (toSelect == null)
return;
list.SetSelectedItems(toSelect);
}
#endregion
}
See this
If you set your ItemsSource to a collection, and the objects in the collection implement INPC, you can set the Style on ListViewItem to use the bound objects IsSelected property.
See this answer to understand what I mean.
Let's say you have ItemsSource="{Binding Items}", then you can do something like:
Items.Where(item => item.IsSelected == true);
to return your list of items that are selected.
You could also do Items[0].IsSelected = true; to programmatically select an item.
In short, you shouldn't have to use a custom control to implement multiple selection on a ListView.
EDIT
I experienced the virtualization problem when I was implementing Telerik's RadGridView. I used a behavior to counter this problem and it seems to have worked:
public class RadGridViewExt : Behavior<RadGridView>
{
private RadGridView Grid
{
get
{
return AssociatedObject as RadGridView;
}
}
public INotifyCollectionChanged SelectedItems
{
get { return (INotifyCollectionChanged)GetValue(SelectedItemsProperty); }
set { SetValue(SelectedItemsProperty, value); }
}
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.Register("SelectedItems", typeof(INotifyCollectionChanged), typeof(RadGridViewExt), new PropertyMetadata(OnSelectedItemsPropertyChanged));
private static void OnSelectedItemsPropertyChanged(DependencyObject target, DependencyPropertyChangedEventArgs args)
{
var collection = args.NewValue as INotifyCollectionChanged;
if (collection != null)
{
collection.CollectionChanged += ((RadGridViewExt)target).ContextSelectedItemsCollectionChanged;
}
}
protected override void OnAttached()
{
base.OnAttached();
Grid.SelectedItems.CollectionChanged += GridSelectedItemsCollectionChanged;
}
void ContextSelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
UnsubscribeFromEvents();
Transfer(SelectedItems as IList, AssociatedObject.SelectedItems);
SubscribeToEvents();
}
void GridSelectedItemsCollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
UnsubscribeFromEvents();
Transfer(AssociatedObject.SelectedItems, SelectedItems as IList);
SubscribeToEvents();
}
private void SubscribeToEvents()
{
AssociatedObject.SelectedItems.CollectionChanged += GridSelectedItemsCollectionChanged;
if (SelectedItems != null)
{
SelectedItems.CollectionChanged += ContextSelectedItemsCollectionChanged;
}
}
private void UnsubscribeFromEvents()
{
AssociatedObject.SelectedItems.CollectionChanged -= GridSelectedItemsCollectionChanged;
if (SelectedItems != null)
{
SelectedItems.CollectionChanged -= ContextSelectedItemsCollectionChanged;
}
}
public static void Transfer(IList source, IList target)
{
if (source == null || target == null)
return;
target.Clear();
foreach (var o in source)
{
target.Add(o);
}
}
}
Inside RadGridView control:
<i:Interaction.Behaviors>
<local:RadGridViewExt SelectedItems="{Binding SelectedItems}" />
</i:Interaction.Behaviors>
where
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
and remember to add reference to Interactivity assembly.
ListBox has a property called SelectionMode. You can set it to Multiple to enable multiple selection.
<ListBox x:Name="MyListBox" SelectionMode="Multiple"/>
You can use SelectedItems property to get all the selected items afterwards.

Why wont UI update on RaisePropertyChanged?

I have a static IList which acts as a repository in a static class:
//static class Settings
public static IList RecentSearchedRepo = new ObservableCollection<object>();
and an IList located in another class which I bind my UI grid to :
//component class
private IList recentsearch = new ObservableCollection<object>();
public IList RecentSearch
{
get
{
return recentsearch;
}
set
{
recentsearch = value;
RaisePropertyChanged("RecentSearch");
}
}
I add objects to RecentSearchedRepo :
RecentSearchedRepo.add(searchitem)
then set RecentSearch to the static list
RecentSearch = Settings.RecentSearchedRepo;
XAML snippet:
<GridLayout:RecentSearchGrid x:Name="recentSearchGrid" ItemsSource="{Binding RecentSearch}" />
snippet from RecentSearchGrid class which extends UserControl:
public IList ItemsSource
{
get
{
return GetValue(ItemsSourceProperty) as IList;
}
set
{
SetValue(ItemsSourceProperty, value);
}
}
private static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof(IList), typeof(RecentSearchGrid), new PropertyMetadata(null, OnItemsSourcePropertyChanged));
private static void OnItemsSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
RecentSearchGrid source = d as RecentSearchGrid;
if (source != null)
{
source.setListforgrid(source.ItemsSource);
}
}
The problem is when I add the first item to RecentSearchedRepo the UI is updated , but on every subsequent add the UI does not update.
Instead of:
RecentSearch = Settings.RecentSearchedRepo;
Try doing:
RecentSearch.Clear();
var freshData = Settings.RecentSearchedRepo;
if (freshData != null)
foreach (var item in freshData)
RecentSearch.Add(item);
You were killing the binding by reassigning the reference.
EDIT: After yours
You're doing it backwards: that OnItemsSourcePropertyChanged shouldn't be setting the source, it shouldn't be there at all actually.
You must bind, in RecentSearchGrid.xaml, to the ItemsSource dependency property declared in RecentSearchGrid.xaml.cs
I don't think that there's enough information here to answer your question. The following simple application mirrors the scenario that I see described in the question and it works as expected:
// MySettings.cs
public static class MySettings
{
public static IList RecentSearchedRepo = new ObservableCollection<object>();
}
// MyVm.cs
public class MyVm : INotifyPropertyChanged
{
private IList recentSearch = new ObservableCollection<object>();
public event PropertyChangedEventHandler PropertyChanged;
public MyVm()
{
this.RecentSearch = MySettings.RecentSearchedRepo;
}
public IList RecentSearch
{
get { return recentSearch; }
set
{
recentSearch = value;
this.RaisePropertyChanged("RecentSearch");
}
}
private void RaisePropertyChanged(string p)
{
if (this.PropertyChanged != null) this.PropertyChanged(this, new PropertyChangedEventArgs(p));
}
}
// MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
this.InitializeComponent();
// Initialization as described in the question
MySettings.RecentSearchedRepo.Add("SearchItem1");
MySettings.RecentSearchedRepo.Add("SearchItem2");
this.DataContext = new MyVm();
}
private void Button_Click_1(object sender, RoutedEventArgs e)
{
// Add a new item later
MySettings.RecentSearchedRepo.Add("NewlyAddedSearchItem");
}
}
// MainWindow.xaml
<Window x:Class="ScratchWpf.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<DockPanel>
<Button DockPanel.Dock="Bottom" Content="Add new Search Item" Click="Button_Click_1" />
<ListBox ItemsSource="{Binding RecentSearch}" />
</DockPanel>
</Window>
I'm going to try putting on my psychic hat and ask if, perhaps, you are adding additional items to the wrong collection. Does the collection get recreated and placed at the binding after a single item is added, but later items are added to the original collection instead of the new one?
Given that you stated RecentSearchGrid is a UserControl, we can also infer that the implementation of ItemsSource may be a custom one rather than the standard one that would be inherited from an ItemsControl. It's possible that the RecentSearchGrid is breaking the binding incorrectly in there somehow.
I agree with Baboon. What is the purpose of OnItemsSourcePropertyChanged? In a typical implementation, I wouldn't expect that to be there.
The problem may be as follow:
OnItemsSourcePropertyChanged will not get called, if the instance does not change.
From WPF point of view, when you RaisePropertyChangeEvent, but the instance of the bound collection does not change, PropertyChange handler will not be called at all.
Is Settings.RecentSearchedRepo the same instance through the lifetime of the app?

Categories