I'm using a WPF ListView to show and select/deselect some items but there are some conditions that user cannot select/deselect an item and I should handle it in the code behind.
I tried to use ListView.SelectionChanged event to handle it but the problem is that when I change the selected item in the code behind (select it again if it was deselected or the other way), the event triggers again and I don't want that.
What is the best way to set conditions on select/deselect ListView item?
I tried to solve it using ListView.PreviewMouseLeftButtonDown event instead of ListView.SelectionChanged. But I wanted to know if there is a better way?
There might be other WPF controls (especially third party ones) that can handle this a little more gracefully. Additionally you can hook into the style of the ListView to alter the mouse-over behavior.
As a crude approach; however, I was able to accomplish what you're looking for by using the SelectionChanged event to effectively "undo" any selections we deem invalid.
First a simple ListView in xaml:
<ListView
x:Name="ItemsLv"
SelectionChanged="ItemsLv_SelectionChanged"
SelectionMode="Single" />
The SelectionMode being Single is important here as the approach for undoing a selection when multiple items are selected is more complicated.
Then, an object to represent our list items with the ToString() overload implemented so we don't have to fiddle with data templates and binding.
public class MyListViewItem
{
public string Text { get; set; }
public bool IsSelectable { get; set; }
public override string ToString()
{
return Text;
}
}
This object just represents a string (our data) paired with a boolean of whether or not we want this item to be selectable.
Then, a quick little setup to populate the list with a few item for testing:
public MainWindow()
{
InitializeComponent();
var items = new List<MyListViewItem>
{
new() { Text = "Item One", IsSelectable = true },
new() { Text = "Item Two", IsSelectable = true },
new() { Text = "Item Three", IsSelectable = false },
new() { Text = "Item Four", IsSelectable = true }
};
ItemsLv.ItemsSource = items;
}
And finally, the "magic". The SelectionChanged event handler:
private void ItemsLv_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// if this event wasn't caused by a new item that wasn't already
// selected being selected, don't do anything extra
if (e.AddedItems.Count <= 0)
{
return;
}
// get the newly selected item and cast it to a MyListViewItem so we can inspect the IsSelectable property
var selectedItem = (MyListViewItem)ItemsLv.SelectedItem;
// if it's a selectable item, we don't need to intervene
if (selectedItem.IsSelectable)
{
return;
}
// we made it this far that means we tried to select an item that should NOT be selectable
// if the new selected item caused us to UNselect an old item, put the selection back
if (e.RemovedItems.Count > 0)
{
ItemsLv.SelectedItem = e.RemovedItems[0];
}
// otherwise (the first selection ever?) just set selection back to null
else
{
ItemsLv.SelectedItem = null;
}
}
Hopefully the code comments in there make it clear what's going on.
Related
I have a WinForms Dialog with 2 ListBox controls. In the application under test, doubleClicking any items in one of the listbox controls (I'll call this the CONTROL LISTBOX) results in selecting the matching item in the other listbox (SLAVE LISTBOX).
My test causes multiple entries to be made in the CONTROL LISTBOX. The test then performs a ListBox.SelectedItem.DoubleClick() on each of the CONTROL lISTBOX items, comparing the ListBox.SelectedItemText from both listbox controls.
In the application UI, this ALWAYS works, but the test of the call to ListBox.SelectedItemText for SLAVE LISTBOX returns the text matching what is slected in the UI correctly ONLY on the initial iteration of the doubleclick\compare.
Can anybody help me figure out what I'm doing wrong? Thanks!
Here is my code:
public bool SelectMainEventViaErrorEvent(int eventIdx)
{
bool bSuccess = false;
errorEvents.Items.Select(eventIdx);
System.Threading.Thread.Sleep(1000);
errorEvents.Items.SelectedItem.DoubleClick();
System.Threading.Thread.Sleep(1000);
if (eventIdx > 0)
{
IVScrollBar vertScroll = mainEvents.ScrollBars.Vertical;
vertScroll.ScrollDownLarge();
}
if (errorEvents.SelectedItemText == mainEvents.SelectedItemText)
{
bSuccess = true;
}
log.Info($"SelectMainEventViaErrorEvent({eventIdx}) selected error event = {errorEvents.SelectedItemText}");
log.Info($"SelectMainEventViaErrorEvent({eventIdx}) selected main event = {mainEvents.SelectedItemText}");
return bSuccess;
}
As you can see, by the following image, the text in both list boxes are identical. However, the call to ListBox.SelectedItemText for the top listbox (SLAVE LISTBOX) returns the value from the first iteration, which matched the first item in the bottom listbox (CONTROL LISTBOX) during the first iteration of the doubleclick/compare.
Proof that the text of the selected listbox items match
Comparing with plain text is bad idea since "Text" != "Text ". What you could use in yor case is DisplayMember and ValueMember properties.
I will demonstrate it for you with manually populating listboxes but you do it from database or however you do it.
First of all create class that will store your values and it's ID's. I usually create it like this (so i am able to use that class later for something else)
public class Int_String
{
public int _int { get; set; } // Important to be declared like properties and not like variables
public string _string { get; set; }
}
Now let's populate our listbox like this:
public YourForm()
{
List<Int_String> list = new List<Int_String>();
list.Add(new Int_String { _int = 1, _string = "Some text" }); // I am populating it manually but you will populate it from DB or somewhere else
list.Add(new Int_String { _int = 2, _string = "Some other text" });
list.Add(new Int_String { _int = 3, _string = "One more text" });
// Now when we have list we need to bind it to our listbox.
// IMPORTANT!!!!!
// Display member and Value member properties SHOULD be able to be added before and after assigning datasource to control (like combobox) BUT for some reason on listbox it only works when you assign it AFTER you bind your datasource to listbox.
// If you ever work with other controls and use these values, ALWAYS declare display member and value member BEFORE you bind datasource. Why? For now let's just say it is much faster but explanation is for other question
myListBox1.DataSource = list;
myListBox1.DisplayMember = "_string"; // When you start typing .DisplayMember, you will not get that property in recommendation since it is hidden so do not think there is not that property there.
myListBox1.ValueMember = "_int"; // Same as above
}
Now when you populate listbox like this and second one on same way with same id's you could simply do if(listbox1.SelectedValue == listbox2.SelectedValue) and compare them even if their text is not equal but id is.
BONUS:
Also what you can do is expand class like this:
public class Int_String
{
public int _int { get; set; }
public string _string { get; set; }
public string SomethingOther = "AsD";
public bool IsTrue()
{
return true;
}
}
then bind it on same way and do this:
Int_String item = listbox1.SelectedItem as Int_String;
bool check = item.IsTrue();
MessageBox.Show(item.SomethingOther);
So basically you bind whole class for each item in listbox, display to user one of the variables (in our case _string), set ValueMember to other unique variable so it is easy to search whole listbox and when needed get whole class from that item.
I wasn't ever able to get this working by iterating forward through the errorEvent listbox in my automated test code, but it DOES work when iterating backwards through the errorEvent listbox.
Calling code:
for (int i = eventViewer.GetErrorEventsCount() - 1; i >= 0; i--)
{
bResult = eventViewer.SelectMainEventViaErrorEvent(i);
if (!bResult)
{
break;
}
System.Threading.Thread.Sleep(2000);
}
Verification Code:
public bool SelectMainEventViaErrorEvent(int eventIdx)
{
bool bSuccess = false;
DisableToolTips(true);
errorEvents.Select(eventIdx);
errorEvents.SelectedItem.DoubleClick();
System.Threading.Thread.Sleep(1000);
log.Info($"SelectMainEventViaErrorEvent({eventIdx}) selected error event = {errorEvents.SelectedItemText}");
log.Info($"SelectMainEventViaErrorEvent({eventIdx}) selected main event = {mainEvents.SelectedItemText}");
if (errorEvents.SelectedItemText == mainEvents.SelectedItemText)
{
bSuccess = true;
}
return bSuccess;
}
Some items in the ListView control will be selectable and have normal text.
Some items however, although included in the ListView as items, will be unselectable/unclickable and 'greyed-out'.
In Windows-Store-Apps we have the ability to select Single/Multiple/None items in a ListView. But how can make certain items at certain indexes unselectable/unclickable and 'greyed-out', in code mainly?
I managed to access the Item of the ListView at a certain index:
myListView.ItemContainerGenerator.ContainerFromIndex(i)
But I couldn't find any option to customize its selected event handler.
Any idea how to achieve that?
In Single selection mode.
First Add a boolean property to class of binding type which defines which items are clickable like this
class TestClass
{
Boolean IsClickAllowed{get;set;}
string name{get;set;}
}
then create a source list of TestClass type and set it as itemssource of Listview like this
var TempList=new List<>()
{
new TextClass(){IsClickAllowed=false,name="First Item"},
new TextClass(){IsClickAllowed=true,name="Second Item"},
new TextClass(){IsClickAllowed=false,name="Third Item"},
};
MyList.ItemsSource=TempList;
and for greying out Set Different DataTemplate for nonClickable items implementing DataTemplateSelector and finally for click handle in ItemClick event. You need to set IsItemClickEnabled as true.
private void MyList_ItemClick(object sender, ItemClickEventArgs e)
{
var item = e.ClickedItem as TestClass;
if (item != null){
if(item.IsClickAllowed){
//Do Stuff here
}else
{
//Do Nothing
}
}}
Hope it helps.
I have found a solution:
I have override the ListView control and create a StripedListView. Then by overriding the PrepareContainerForItemOverride, which is responsible for the setting up the ListViewItem control after it’s be created, you could modify the background color and set the ItemListView.isEnabled option to false:
public class StripedListView : ListView
{
protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
{
base.PrepareContainerForItemOverride(element, item);
var listViewItem = element as ListViewItem;
if (listViewItem != null)
{
var index = IndexFromContainer(element);
if (Words.arrayW[index].Length > 0)
{
listViewItem.Foreground = new SolidColorBrush(Colors.Black);
}
else
{
listViewItem.Foreground = new SolidColorBrush(Colors.Gray);
listViewItem.IsEnabled = false;
}
}
}
}
In Xaml:
<controls:StripedListView x:Name="letterListView" ItemsSource="{Binding}">
<controls:StripedListView.ItemTemplate>
<DataTemplate>
etc...
</DataTemplate>
</controls:StripedListView.ItemTemplate>
</controls:StripedListView>
I am using a ListView control to display some lines of data. There is a background task which receives external updates to the content of the list. The newly received data may contain less, more or the same number of items and also the items itself may have changed.
The ListView.ItemsSource is bound to an OberservableCollection (_itemList) so that changes to _itemList should be visible also in the ListView.
_itemList = new ObservableCollection<PmemCombItem>();
_itemList.CollectionChanged += new NotifyCollectionChangedEventHandler(OnCollectionChanged);
L_PmemCombList.ItemsSource = _itemList;
In order to avoid refreshing the complete ListView I do a simple comparison of the newly retrieved list with the current _itemList, change items which are not the same and add/remove items if necessary. The collection "newList" contains newly created objects, so replacing an item in _itemList is correctly sending a "Refresh" notification (which I can log by using the event handler OnCollectionChanged of the ObservableCollection`)
Action action = () =>
{
for (int i = 0; i < newList.Count; i++)
{
// item exists in old list -> replace if changed
if (i < _itemList.Count)
{
if (!_itemList[i].SameDataAs(newList[i]))
_itemList[i] = newList[i];
}
// new list contains more items -> add items
else
_itemList.Add(newList[i]);
}
// new list contains less items -> remove items
for (int i = _itemList.Count - 1; i >= newList.Count; i--)
_itemList.RemoveAt(i);
};
Dispatcher.BeginInvoke(DispatcherPriority.Background, action);
My problem is that if many items are changed in this loop, the ListView is NOT refreshing and the data on screen stay as they are...and this I don't understand.
Even a simpler version like this (exchanging ALL elements)
List<PmemCombItem> newList = new List<PmemCombItem>();
foreach (PmemViewItem comb in combList)
newList.Add(new PmemCombItem(comb));
if (_itemList.Count == newList.Count)
for (int i = 0; i < newList.Count; i++)
_itemList[i] = newList[i];
else
{
_itemList.Clear();
foreach (PmemCombItem item in newList)
_itemList.Add(item);
}
is not working properly
Any clue on this?
UPDATE
If I call the following code manually after updating all elements, everything works fine
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
But of course this causes the UI to update everything which I still want to avoid.
After a change, you can use the following to refresh the Listview, it's more easy
listView.Items.Refresh();
This is what I had to do to get it to work.
MyListView.ItemsSource = null;
MyListView.ItemsSource = MyDataSource;
I know that's an old question, but I just stumbled upon this issue. I didn't really want to use the null assignation trick or the refresh for just a field that was updated.
So, after looking at MSDN, I found this article:
https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.inotifypropertychanged?redirectedfrom=MSDN&view=netframework-4.7.2
To summarize, you just need the item to implement this interface and it will automatically detect that this object can be observed.
public class MyItem : INotifyPropertyChanged
{
private string status;
public string Status
{
get => status;
set
{
OnPropertyChanged(nameof(Status));
status = value;
}
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
So, the event will be called everytime someone changes the Status. And, in your case, the listview will add a handler automatically on the PropertyChanged event.
This doesn't really handle the issue in your case (add/remove).
But for that, I would suggest that you have a look at BindingList<T>
https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.bindinglist-1?view=netframework-4.7.2
Using the same pattern, your listview will be updated properly without using any tricks.
You should not reset ItemsSource of ListView each time observable collection changed. Just set proper binding that will do your trick. In xaml:
<ListView ItemsSource='{Binding ItemsCollection}'
...
</ListView>
And in code-behind (suggest to use MVVM) property that will be responsible for holding _itemList:
public ObservableCollection<PmemCombItem> ItemsCollection
{
get
{
if (_itemList == null)
{
_itemList = new ObservableCollection<PmemCombItem>();
}
return _itemList;
}
}
UPDATE:
There is similar post which most probably will Answer your question: How do I update an ObservableCollection via a worker thread?
I found a way to do it. It is not really that great but it works.
YourList.ItemsSource = null;
// Update the List containing your elements (lets call it x)
YourList.ItemsSource = x;
this should refresh your ListView (it works for my UAP :) )
An alternative on Xopher's answer.
MyListView.ItemsSource = MyDataSource.ToList();
This refreshes the Listview because it's a other list.
Please check this answer:
Passing ListView Items to Commands using Prism Library
List view Items needs to notify about changes (done is setter)
public ObservableCollection<Model.Step> Steps
{
get { return _steps; }
set { SetProperty(ref _steps, value); }
}
and UpdateSourceTrigger need to be set in xaml
<Image Source="{Binding ImageData, UpdateSourceTrigger=PropertyChanged}" />
I have a listview with a checkbox on each row and I need to make it so that if the user exits the app and later comes back to that liistview the same items remain checked, I have succesfully saved the checked items, but If I try to recheck them in the getview() method the list starts to lag, and random checkboxes start getting checked.
Below is my code that rechecks the boxes
using (VehicleFeaturesDB vfdb = new VehicleFeaturesDB())
{
selectedfeatures = vfdb.GetSelectedFeatures(Selector.vehicleId);
if (listOfSelectedFeatures != null)
{
foreach (Features f in listOfSelectedFeatures)
{
if (feature.FeatureID == f.FeatureID)
{
CheckBox.Checked = true;
}
}
}
}
Tha
If that using block is in GetView(), you're connecting to the database and pulling the list of features every time a new item is displayed. That's why you're seeing the lag.
You're also not setting Checked to false if the feature is not in the list, so when you reuse the convertview the checkbox may already be checked. That's why you're seeing the random boxes checked.
I would probably get the selected features in your adapter's constructor, then reference that list in your GetView(). Something like this should work for you.
public class MyAdapter...
{
private IEnumerable<SelectedFeature> selectedFeatures;
public MyAdapter()
{
...your code...
using (VehicleFeaturesDB vfdb = new VehicleFeaturesDB())
{
selectedfeatures = vfdb.GetSelectedFeatures(Selector.vehicleId);
}
}
public override View GetView(int pos, View convertView, ViewGroup parent)
{
...your code...
CheckBox.Checked = selectedFeatures.Any(sf => sf.FeatureID == feature.FeatureID);
}
I have a BindingList< KeyValuePair < string, string > > that is bound to a ComboBox control. Based on some conditions, the BindingList will be added a new KeyValuePair. Now, the Newly added item shows up at index 0 of the Combobox, instead of at the end.
While debugging, I found that the BindingList has got the right order. (i.e, the new KeyValuePair is appended)
Also, I check the SelectedValue of the ComboBox in it's SelectedIndexChanged handler and it seems to be not of the ListItem that got selected. Instead, it is that of the supposed ListItem, if the ComboBox had got the right order as in its DataSource, - the BindingList..
The code is a small part of a large project.. Plz let me know if the question is not clear. I can put the relevant parts of the code as per our context.
How could something like this happen? What can I do differently?
I have this class something like this.
public class DropdownEntity
{
//removed all except one members and properties
private string frontEndName
public string FrontEndName
{
get {return this.frontEndName; }
set {this.frontEndName= value; }
}
//One Constructor
public DropdownEntity(string _frontEndName)
{
this.FrontEndName = _frontEndName;
//Removed code which initializes several members...
}
//All methods removed..
public override string ToString()
{
return frontEndName;
}
}
In my windows form, I have a tab control with several tabs. In one of the tabs pages, I have a DataGridView. The user is supposed to edit the cells and click on a Next - button. Then, some processing will be done, and the TabControl will be navigated to the next tab page.
The next tab page has the combobox that has the problem I mentioned. This page also has a back button, which will take back.. the user can modify the gridview cells again.. and click on the next button. This is when the order gets messed up.
I am posting here the Click event handler of the Next Button.. Along with the class, with the rest of the code removed.
public partial class AddUpdateWizard : Form
{
//Removed all members..
BindingList<KeyValuePair<string, string>> DropdownsCollection;
Dictionary<string, DropdownEntity> DropdownsDict;
//Defined in a partial definition of the class..
DataGridView SPInsertGridView = new DataGridView();
ComboBox DropdownsCmbBox = new ComboBox();
Button NextBtn2 = new Button();
Button BackBtn3 = new Button();
//Of course these controls are added to one of the panels
public AddUpdateWizard(MainForm mainForm)
{
InitializeComponent();
DropdownsDict = new Dictionary<string, DropdownEntity>();
}
private void NextBtn2_Click(object sender, EventArgs e)
{
string sqlArgName;
string frontEndName;
string fieldType;
for (int i = 0; i < SPInsertGridView.Rows.Count; i++)
{
sqlArgName = "";
frontEndName = "";
fieldType = "";
sqlArgName = SPInsertGridView.Rows[i].Cells["InsertArgName"].Value.ToString().Trim();
if (SPInsertGridView.Rows[i].Cells["InsertArgFrontEndName"].Value != null)
{
frontEndName = SPInsertGridView.Rows[i].Cells["InsertArgFrontEndName"].Value.ToString().Trim();
}
if (SPInsertGridView.Rows[i].Cells["InsertArgFieldType"].Value != null)
{
fieldType = SPInsertGridView.Rows[i].Cells["InsertArgFieldType"].Value.ToString().Trim();
}
//I could have used an enum here, but this is better.. for many reasons.
if (fieldType == "DROPDOWN")
{
if (!DropdownsDict.ContainsKey(sqlArgName))
DropdownsDict.Add(sqlArgName, new DropdownEntity(frontEndName));
else
DropdownsDict[sqlArgName].FrontEndName = frontEndName;
}
else
{
if (fieldType == "NONE")
nonFieldCount++;
if (DropdownsDict.ContainsKey(sqlArgName))
{
DropdownsDict.Remove(sqlArgName);
}
}
}
//DropdownsCollection is a BindingList<KeyValuePair<string, string>>.
//key in the BindingList KeyValuePair will be that of the dictionary.
//The value will be from the ToString() function of the object in the Dictionary.
DropdownsCollection = new BindingList<KeyValuePair<string,string>>(DropdownsDict.Select(kvp => new KeyValuePair<string, string>(kvp.Key, kvp.Value.ToString())).ToList());
DropdownsCmbBox.DataSource = DropdownsCollection;
DropdownsCmbBox.DisplayMember = "Value";
DropdownsCmbBox.ValueMember = "Key";
//Go to the next tab
hiddenVirtualTabs1.SelectedIndex++;
}
private void BackBtn3_Click(object sender, EventArgs e)
{
hiddenVirtualTabs1.SelectedIndex--;
}
//On Selected Index Changed of the mentioned Combobox..
private void DropdownsCmbBox_SelectedIndexChanged(object sender, EventArgs e)
{
if (DropdownsCmbBox.SelectedValue != null)
{
if (DropdownsDict.ContainsKey((DropdownsCmbBox.SelectedValue.ToString())))
{
var dropdownEntity = DropdownsDict[DropdownsCmbBox.SelectedValue.ToString()];
DropdownEntityGB.Text = "Populate Dropdowns - " + dropdownEntity.ToString();
//Rest of the code here..
//I see that the Datasource of this ComboBox has got the items in the right order.
// The Combobox's SelectedValue is not that of the selected item. Very Strange behavior!!
}
}
}
}
The very first time the user clicks the Next Button, it's fine. But if he clicks the Back Button again and changes the Data Grid View cells.. The order will be gone.
I know, it can be frustrating to look at. It's a huge thing to ask for help. Any help would be greatly appreciated!
Please let me know if you need elaboration at any part.
Thanks a lot :)
I think you have two problems here.
First, if you want to retain the order of the items you should use an OrderedDictionary instead of a regular one. A normal collection will not retain the order of the items when you use Remove method. You can see more info about this related to List here.
You could use such dictionary like this:
DropDownDict = new OrderedDictionary();
// Add method will work as expected (as you have it now)
// Below you have to cast it before using Select
DropDownCollection = new BindingList<KeyValuePair<string, string>>(DropDownDict.Cast<DictionaryEntry>().Select(kvp => new KeyValuePair<string, string>(kvp.Key.ToString(), kvp.Value.ToString())).ToList());
The second problem could be that you change the display name (FrontEndName) of already existing items, but the key is preserved. When you add a new item, try to remove the old one that you're not using anymore and add a new item.
The Sorted Property of the Combobox is set to True! I didn't check that until now. I messed up. Terribly sorry for wasting your time Adrian. Thanks a lot for putting up with my mess here.. :)