I have a datagrid where every column has a combobox as header. Each combobox has its source binded to an observable collection of string. I've made all of this by code behind, since the number of columns of the datagrid are unknown at design time.
When the user choose an item, in each combobox, that item should be disabled after the selection changed. So i tried to do a loop like this:
private void Test_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (vm.myArray == null)
{ vm.myArray = new string[myGrid.Columns.Count]; }
ComboBox cb = sender as ComboBox;
DataGridColumnHeader parent = cb.Parent as DataGridColumnHeader;
int index = parent.Column.DisplayIndex;
string value = cb.SelectedItem as string;
vm.mYArray[index] = value;
foreach(DataGridColumn c in griglia.Columns)
{
foreach(string s in vm.myArray)
{
if(s != null && s != string.Empty)
{
ComboBox dg = c.Header as ComboBox;
for (int i = 0; i < dg.Items.Count; i++)
{
ComboBoxItem it = (ComboBoxItem)dg.ItemContainerGenerator.ContainerFromIndex(i);
if ((string)it.Content == s)
it.IsEnabled = false;
else
it.IsEnabled = true;
}
}
}
}
}
The problem is that when the loop on the columns reach the second iteration, my code raise an exception. After a deep look into my local variables, i noticed that ItemContainerGenerator.Status is NotStarted everywhere except for the combobox in the first column. Can you provide me an help on how to solve this problem?
It seems that I've found a solution. I needed to add this piece of code in the inner loop:
if(dg.ItemContainerGenerator.Status == GeneratorStatus.NotStarted)
{
dg.IsDropDownOpen = true;
this.UpdateLayout();
dg.IsDropDownOpen = false;
}
It seems the problem was that the ItemContainerGenerator is not generated until every ComboBoxItem is not generated. To do this you should trick the UI to believe that each ComboBox was opened at least once.
If you know better answer please let me know.
Related
I am currently developing a WPF app in which there are multiple forms to manage a database. One of these forms is a "Add form". This form contains a combobox with some values. Though if the user's wanted value is not in the combobox list, it can be added by selecting the last option which will display a new form for entering the wanted value. If the form is submitted, the last option in the combobox will be changed to this value. The code below describes how this works:
private void BrandBoxSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (BrandBox.SelectedItem != null &&
BrandBox.SelectedIndex < BrandBox.Items.Count - 1)
{
InitSubCategoryBox(subCatArray[BrandBox.SelectedIndex]);
}
else if (BrandBox.SelectedItem != null &&
BrandBox.SelectedIndex >= BrandBox.Items.Count - 1)
{
OtherInputWindow newInputForm = new OtherInputWindow("brand", BrandBox.SelectedItem.ToString());
if (newInputForm.ShowDialog() == true)
{
BrandBox.Items[BrandBox.Items.Count - 1] = newInputForm.returnValue;
BrandBox.SelectedIndex = BrandBox.Items.Count - 1;
}
}
}
The last code line will set the submitted value as selected item. I know this does not work. This last line of code will create a SelectionChanged event and that is exactly what this method is. The result is that it will keep looping and there is no way out. I'm searching for the right way to do this for a long time but I wasn't able yet to find the answer. Hopefully I will find the answer here. Is there someone who can help me solving this problem?
I would suggest to not change the SelectedItem from the SelecetionChanged event handler. Instead handle the UIElement.PreviewMouseLeftButtonUp event from the ComboBox:
<ComboBox PreviewMouseLeftButtonUp="OnComboBoxItemPreviewMouseLeftButtonUp" />
private void OnComboBoxItemPreviewMouseLeftButtonUp(object sender, RoutedEventArgs e)
{
var selector = sender as Selector;
var clickedItemContainer = Selector.ContainerFromElement(selector, e.OriginalSource as DependencyObject);
bool isClickTargetItemContainer = clickedItemContainer is not null;
if (!isClickTargetItemContainer)
{
return;
}
int selectedIndex = selector.ItemContainerGenerator.IndexFromContainer(clickedItemContainer);
int lastIndex = selector.Items.Count - 1;
if (selector.SelectedIndex == lastIndex)
{
var newInputForm = new OtherInputWindow("brand", selectedItem.ToString());
if (newInputForm.ShowDialog() == true)
{
selector.Items[lastIndex] = newInputForm.returnValue;
selector.SelectedIndex = lastIndex;
}
}
else
{
InitSubCategoryBox(subCatArray[selector.SelectedIndex]);
}
}
I'm working with a ListView in Virtual Mode (.NET 4.6).
I tried to find the index of Items in the virtual ListView: when I enter a letter, the first item with text that start with that letter should be selected.
Here is the FindItemWithText in listView1_KeyDown:
if (char.IsLetterOrDigit(e.KeyChar))
{
var str = e.KeyChar.ToString();
if (tempStr != str)
{
Index = 0;
tempStr = str;
}
var item = listView1.FindItemWithText(str, false, Index, true);
if (item != null)
{
item.Selected = true;
item.Focused = true;
item.EnsureVisible();
Index = item.Index + 1;
}
}
Here is my SearchForVirtualItem method:
var item = lvis.OfType<ListViewItem>().FirstOrDefault(
i => i.Text.ToLower().StartsWith(e.Text.ToLower()) &&
i.Index >= e.StartIndex);
if (item == null)
{
}
else
{
e.Index = item.Index;
}
If the result is one of the visible items before I scroll at all the code works and I can select the result item. But if the result is not visible and I didn't scroll anything at all the method return null.
But if I scroll to the end of the list even once I can get the index of the item that before I couldn't.
Example: If I have 200 items in a virtual list (populated from a list
of 200 ListViewItem) and only the first 50 are visible, if I press the c
letter and items that start with c letter are among the first 50, they will
be selected.
But if I press x and the items in the virtual ListView are
at the last 50, the method will return null. If I instead scroll the list to
the end and then I press x, the items
that start with x will be selected.
Why I have to show the item at least once to have an index and not having index = -1?
Is this the normal behavior of a virtual ListView or is there something wrong?
Side question, when does a ListView in normal mode become slow? After 100,000 items, or 1,000,000 items?
Edit1:
Here is my listView1_RetrieveVirtualItem code:
private void listView1_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
{
if (lvis.Count > 0)
{
e.Item = lvis[e.ItemIndex];
}
}
I don't use a cache.
I use BackGroundWorker to get the data from a SQLite database; I create ListViewitems and add them to a List (var lvis = new List<ListViewItem>).
The RunWorkerCompleted method:
private void Pl_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
var obj = e.Result;
if (obj != null)
{
RemoveSelection();
lvis = (List<ListViewItem>)obj;
listView1.Items.Clear();
listView1.VirtualListSize = lvis.Count;
listView1.Invalidate();
var No_of_items = listView1.Items.Count + " pin(s)";
count.Text = No_of_items;
tabLabel.Text = GetButton().Text + " | " + No_of_items;
}
}
lvis is the source where the virtual ListView get its data from.
It looks like it's a simple misunderstanding related to the stored ListViewItem Index value: when you create a ListViewItem, you cannot set the Index, so this method to retrieve and return a matching ListViewItem:
private void listView1_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
{
var item = lvis.OfType<ListViewItem>().FirstOrDefault([...]);
e.Index = item.Index;
}
will fail: item.Index is always -1, since was never set when the ListViewItem was created.
That's why the ListView will find Items that have already been shown (these have an Index, the virtual list doesn't need to retrieve them calling SearchForVirtualItem(), it will just call FindItem()).
A simple solution is to use the List.FindIndex() method, instead of finding an Item using FirstOrDefault(). This method returns the index in the List that contains the object that meets the criteria defined by the Predicate<T> argument.
This is the value of e.Index that the ListView.SearchForVirtualItem handler is expecting.
How many items a ListView can hold before it becomes difficult to manage or too slow: without any further specifications, this is a question difficult to answer. It may behave perfectly fine with 100000 items in List mode (as in the example), but setting the View = View.Details may change the scenario completely. Does it have to also handle graphics object? Well, how large? How many handles are needed, in this case? In practice, it's a question you answer yourself testing different scenarios.
The User perspective is also to consider (or should it come first? :). Maybe the list is scrollable with ease, but is it also easy to locate a specific Item?
If you have a lot of Items to present in the UI, you should most probably organize them in sub cathegories and provide easy, quick, visual methods to search and filter them, so your Users end up working with much less crowded subsets, probably closer to what they actually need to use or find.
Here's a fix and a code sample that should allow to test the functionality of the ListView.FindItemWithText() method (this one also needs a small tweak).
The ListView.VirtualMode is set in the Designer
In the example, the ListViewItems collection is represented by a list of 1,000 items, repeated 100 times, so the ListView VirtualListSize is set to 100,000 items
→ btnLVSearch: the Button used to search the ListView items.
→ btnLVLoadData: the Button used to load the data and sets the VirtualListSize.
→ chkPrefixSearch: the CheckBox that selects a PrefixSearch or a TextSearch.
→ chkCaseSensitiveSearch: the CheckBox used to set/reset the case sensitive search
int currentStartIndex = 0;
List<ListViewItem> listItems = null;
private void btnLVLoadData_Click(object sender, EventArgs e)
{
listItems = new List<ListViewItem>();
// [...]
// Fill the listItems collection
listView1.VirtualListSize = listItems.Count;
}
private void listView1_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
{
if (e.ItemIndex >= 0) {
e.Item = listItems[e.ItemIndex];
}
}
private void listView1_SearchForVirtualItem(object sender, SearchForVirtualItemEventArgs e)
{
StringComparison comparison = chkCaseSensitiveSearch.Checked
? StringComparison.CurrentCulture
: StringComparison.CurrentCultureIgnoreCase;
int itemIndex = -1;
if (e.IsPrefixSearch) {
itemIndex = listItems.FindIndex(e.StartIndex,
itm => itm.Text.StartsWith(e.Text, comparison));
}
else if (e.IsTextSearch) {
itemIndex = listItems.FindIndex(e.StartIndex,
itm => itm.Text.IndexOf(e.Text, comparison) >= 0);
}
e.Index = itemIndex;
}
private void btnLVSearch_Click(object sender, EventArgs e)
{
var item = listView1.FindItemWithText(
txtLVSearch.Text, false, currentStartIndex, chkPrefixSearch.Checked);
if (item != null) {
currentStartIndex = item.Index + 1;
listView1.SelectedIndices.Add(item.Index);
item.Selected = true;
listView1.EnsureVisible(item.Index);
listView1.Focus();
}
else {
currentStartIndex = 0;
}
}
When handling the ListView.KeyPress event, set e.Handled = true to suppress the key press, otherwise a second SearchForVirtualItem event is triggered immediately after e.Index = itemIndex is assigned (this time, with e.IsPrefixSearch set to false):
private void listView1_KeyPress(object sender, KeyPressEventArgs e)
{
e.Handled = true;
var item = listView1.FindItemWithText(
e.KeyChar.ToString(), false, currentStartIndex, chkPrefixSearch.Checked);
// [...]
}
private void dgGrid_CellListSelect(object sender, CellEventArgs e)
{
if (e.Cell.Column.Key == "ColumnA")
{
UltraGridRow selectedItem = ((UltraCombo)e.Cell.EditorControlResolved).SelectedRow;
if (selectedItem != null)
{
//Option A
cmbColumnB.DataSource = GetUISender<someBF>().RetrieveData(dataset).dataTable;
cmbColumnB.DataBind();
//Option B
//((UltraCombo)e.Cell.Row.Cells["ChipSetID"].EditorControlResolved).DataSource = GetUISender<someBF>().RetrieveData(dataset).dataTable;
}
}
}
There is a button that allow the datagrid to add new row.
This datagrid have 2 columns and both columns are UltraCombo. ColumnB combobox's dataSource will based on ColumnA. Based on the above code, it works if the datagrid only have 1 row, but once user add another row, both row's ColumnB will be sharing the same DataSource.
How to make sure ColumnB's DataSource stay independently without affecting other rows? It's very obvious that this happened because every row are sharing the same component which is cmbColumnB but I'm not sure on how to remove the reference
I've found the solution which is everytime create a new UltraCombo and bind it to the particular cell's EditorControl
private void dgGrid_CellListSelect(object sender, CellEventArgs e)
{
if (e.Cell.Column.Key == "ColumnA")
{
UltraGridRow selectedItem = ((UltraCombo)e.Cell.EditorControlResolved).SelectedRow;
if (selectedItem != null)
{
UltraCombo cmbValue = new UltraCombo();
cmbValue.LimitToList = true;
cmbValue.DropDownStyle = UltraComboStyle.DropDownList;
cmbValue.DataSource = GetUISender<someBF>().RetrieveData(dataset).dataTable;
cmbValue.ValueMember = someDS.someDT.someColumnIDColumn.ColumnName;
cmbValue.DisplayMember = someDS.someDT.someColumnDescriptionColumn.ColumnName;
cmbValue.BindingContext = someDg.BindingContext;
cmbValue.DataBind();
e.Cell.Row.Cells["ColumnB"].EditorControl = cmbValue;
e.Cell.Row.Cells["ColumnB"].Style = Infragistics.Win.UltraWinGrid.ColumnStyle.DropDownList;
}
}
}
I've a custom combobox template because of some binding stuff that won't work with the 'default' ComboBoxColumn.
To make it look 'nice' I've one template for the edit mode (a Combobox) and one for the 'normal' mode (a Label).
Now, because of that I've to commit the edit made to the combobox manually inside the CellEditEnding event
private bool changeCommitInProgress = false;
private void table_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
{
if (e.EditingElement is ContentPresenter && e.EditAction == DataGridEditAction.Commit)
{
if (!changeCommitInProgress)
{
changeCommitInProgress = true;
DataGrid grid = (DataGrid)sender;
grid.CommitEdit(DataGridEditingUnit.Row, false);
changeCommitInProgress = false;
}
}
}
The problem with this is, that it'll remove the focus from the entire datagrid. Just to be on the safe side, these are the only properties I changed on the datagrid (aside from the Name property and the ItemsSource):
grid.AutoGenerateColumns = false;
grid.IsSynchronizedWithCurrentItem = true;
grid.SelectionUnit = DataGridSelectionUnit.Cell;
This is a fun question. I have done similar with a nested DataList where I had to add a new row after last entry and focus on the first textbox of the newly generated row, maybe you can extrapolate my strategy to fit your situation?
protected void calcAvg(object sender, CommandEventArgs e)
{
int row = Convert.ToInt32(e.CommandArgument.ToString()) - 1;
DataListItem ActiveRow = dlMeasurements.Items[row];
// Snipped code doing stuff with current row
// Compare how many rows completed to number of rows requested
if (!(row + 1 == Convert.ToInt32(txtSample.Text)))
{
// Create new row
DataRow drNew = nextMeas.Tables[0].NewRow();
nextMeas.Tables[0].Rows.Add(drNew);
// Change item index and rebind
dlMeasurements.EditItemIndex = row + 1;
dlMeasurements.DataSource = nextMeas.Tables[0];
dlMeasurements.DataBind();
//Set focus with the Script Manager
smInspection.SetFocus((TextBox)(dlMeasurements.Items[row + 1].FindControl("txtRead1")));
}
else
{
// Otherwise close the measurements and show exit button
dlMeasurements.EditItemIndex = -1;
dlMeasurements.DataSource = nextMeas.Tables[0];
dlMeasurements.DataBind();
btnSaveAndPrint.Visible = true;
}
}
}
I have a datagrid that I fill with data from a database.
When I click on a row, I call the GotFocus method and try to make a button visible if certain requirements are met.
private void dtgVerkoopsdocumenten_GotFocus(object sender, RoutedEventArgs e)
{
DataItem row = (DataItem)dtgVerkoopsdocumenten.SelectedItems[0];
if (row.soort2 == "Factuur")
{
btnBoeking.IsHitTestVisible = true;
btnBoeking.Opacity = 1;
}
else
{
btnBoeking.IsHitTestVisible = false;
btnBoeking.Opacity = 0.5;
}
}
This gives me an error.
Index was out of range. Must be non-negative and less than the size of the collection.
Now when I call the code but from a button click it does it how it's supposed to work.
private void tester_Click(object sender, RoutedEventArgs e)
{
DataItem row = (DataItem)dtgVerkoopsdocumenten.SelectedItems[0];
test.Content = row.soort2;
if (row.soort2 == "Factuur")
{
btnBoeking.IsHitTestVisible = true;
btnBoeking.Opacity = 1;
}
else
{
btnBoeking.IsHitTestVisible = false;
btnBoeking.Opacity = 0.5;
}
}
Why is this?
Why dont you use DataGrid SelectedIndexChanged event?
Wyy use GotFocus that doesnt tell you if user even made a selection to start with,
DataItem row = (DataItem)dtgVerkoopsdocumenten.SelectedItems[0];
Called from gotfocus will fail as you have nothing selected besides having no error check in place to check if selection,
If you use Selection changed events you know the user has made selection changes there are a number of events for selection
before access selected items by index you need to check selected item count is grater than zero condition.
Because dtgVerkoopsdocumenten.SelectedItems are empty and GotFocus event raise before SelectedItemChanged event so we are not sure the dtgVerkoopsdocumenten.SelectedItems have any item or not.
You can check dtgVerkoopsdocumenten.SelectedItems before do anything.
if (dtgVerkoopsdocumenten.SelectedItems != null &&
dtgVerkoopsdocumenten.SelectedItems.Count > 0)
{
DataItem row = (DataItem)dtgVerkoopsdocumenten.SelectedItems[0];
...
}