I've searched and searched and searched but can't find an answer.
C# and WPF, I have a single ListView with 5 columns and each column has a TextBox in it.
My custom class
public class SomeThing
{
public String field1 { get; set; }
public String field2 { get; set; }
public String field3 { get; set; }
public String field4 { get; set; }
public String field5 { get; set; }
}
My add code
SomeThing item = new SomeThing();
lstItems.Items.Add(item);
My keydown code
private void TextBox_KeyDown(object sender, KeyEventArgs e)
{
if(e.Key == Key.Return || e.Key == Key.Tab)
{
TextBox tb = (TextBox)sender;
Grid grid = (Grid)tb.Parent;
if (tb.Tag.Equals("Price"))
{
if(lstItems.Items.Count <= lstItems.SelectedIndex + 1) {
SomeThing item = new SomeThing();
lstItems.Items.Add(item);
}
lstItems.SelectedIndex = lstItems.SelectedIndex + 1;
ListViewItem selectedItem = (ListViewItem)lstItems.ItemContainerGenerator.ContainerFromItem(this.lstItems.SelectedItem);
e.Handled = true;
}
}
}
But
ListViewItem selectedItem = (ListViewItem)lstItems.ItemContainerGenerator.ContainerFromItem(this.lstItems.SelectedItem);
Is always null,
this.lstItems.SelectedItem
is just an object of instance "SomeThing".
How do I get the ListView Container?
How do I focus the TextBox on the new selected row?
Please help
It is likely that you are trying to get something from the ItemContainerGenerator that has not been generated yet at the time you ask for it. Adding an item to an ItemsControl (which ListView is a subclass of) does not immediately create a container for that item. There is a delay involved.
This is not really the ideal way to be working with ItemsControl instances. They are really designed around being used with the MVVM design pattern. However, if you need to work with it this way for some reason, then you will need to pay attention to the Status property on the ItemContainerGenerator and the associated StatusChanged event.
So, something along these lines:
if (myItemsControlInstance.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
{
// You should be able to get the container using ContainerFromItem
}
else
{
// You will have to wait
myItemsControlInstance.ItemContainerGenerator.StatusChanged += myItemsControlInstance_StatusChanged;
}
...
void myItemsControlInstance_StatusChanged(object sender, EventArgs e)
{
if (myItemsControlInstance.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
{
myItemsControlInstance.ItemContainerGenerator.StatusChanged -= myEventHandler;
// You should be able to get the container now using ContainerFromItem.
// However, layout hasn't been performed on it yet at this point, so there is
// no guarantee that the item is in good condition to be messed with yet.
LayoutUpdated += app_LayoutUpdated;
}
}
void app_LayoutUpdated(object sender, EventArgs e)
{
LayoutUpdated -= app_LayoutUpdated;
if (myItemsControlInstance.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
{
// Now, you can finally get the container using ContainerFromItem and do something with it.
}
else
{
// It looks like more items needed to be generated...
myItemsControlInstance.ItemContainerGenerator.StatusChanged += myItemsControlInstance_StatusChanged;
}
}
There are a few more things you have to watch out for though when working with ItemContainerGenerator directly like this:
The Status on the generator can be Error. You might want to check for that, though I have never seen it happen.
If the ItemsControl is using a virtualizing panel to contain the items, it is possible that you are trying to access an item that is not in view and therefore doesn't exist. You can either override the type of panel used to not virtualize, such as StackPanel instead of VirtualizingStackPanel by setting the ItemsPanel property, or you can make sure the item is scrolled into view somehow before starting the process (which is a whole separate topic).
My recommendation would be to switch to an MVVM model and read up on how to work with ItemsControl in a more natural way, because doing it this way is complicated and error-prone.
Related
I have an app that retrieves data from a database and displays it in data grid on the main window. The maximum number of items being displayed is ~5000.
I don't mind a time delay in display the results, but i'd like to display a loading animation whilst this is happening. However, even when using a background worker to update the collection view source the UI freezes before displaying the rows.
Is it possible to add all these rows without freezing the UI? Apply filters to the collection view source also seems to freeze the UI which i'd like to avoid also if possible.
Thanks in advance!
UPDATE 06.01.2023
Updated as per the suggestions from BionicCode and Andy and now everything is running very smoothly - thank you for the help!
XAML for the data grid:
<DataGrid Grid.Column="1" Name="documentDisplay" ItemsSource="{Binding Source={StaticResource cvsDocuments}, UpdateSourceTrigger=PropertyChanged, IsAsync=True}" AutoGenerateColumns="False"
Style="{StaticResource DataGridDefault}" ScrollViewer.CanContentScroll="True"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch" ColumnWidth="*">
XAML for collection view source:
<Window.Resources>
<local:Documents x:Key="documents" />
<CollectionViewSource x:Key="cvsDocuments" Source="{StaticResource documents}"
Filter="DocumentFilter">
Code within function being called after retrieving data from database:
Documents _documents = (Documents)this.Resources["documents"];
BindingOperations.EnableCollectionSynchronization(_documents, _itemsLock);
if (!populateDocumentWorker.IsBusy)
{
progressBar.Visibility = Visibility.Visible;
populateDocumentWorker.RunWorkerAsync(jobId);
}
Code within worker:
Documents _documents = (Documents)this.Resources["documents"];
lock (_itemsLock)
{
_documents.Clear();
_documents.AddRange(documentResult.documents);
}
Observable collection class:
public class Documents : ObservableCollection<Document>, INotifyPropertyChanged
{
private bool _surpressNotification = false;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (!_surpressNotification)
{
base.OnCollectionChanged(e);
}
}
public void AddRange(IEnumerable<Document> list)
{
if(list == null)
{
throw new ArgumentNullException("list");
_surpressNotification = true;
}
foreach(Document[] batch in list.Chunk(25))
{
foreach (Document item in batch)
{
Add(item);
}
_surpressNotification = false;
}
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
Base class for observable collection:
public class Document : INotifyPropertyChanged, IEditableObject
{
public int Id { get; set; }
public string Number { get; set; }
public string Title { get; set; }
public string Revision { get; set; }
public string Discipline { get; set; }
public string Type { get; set; }
public string Status { get; set; }
public DateTime Date { get; set; }
public string IssueDescription { get; set; }
public string Path { get; set; }
public string Extension { get; set; }
// Implement INotifyPropertyChanged interface.
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(String info)
{
if(PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
private void NotifyPropertyChanged(string propertyName)
{
}
// Implement IEditableObject interface.
public void BeginEdit()
{
}
public void CancelEdit()
{
}
public void EndEdit()
{
}
}
Filter Function:
private void DocumentFilter(object sender, FilterEventArgs e)
{
//Create list of all selected disciplines
List<string> selectedDisciplines = new List<string>();
foreach(var item in disciplineFilters.SelectedItems)
{
selectedDisciplines.Add(item.ToString());
}
//Create list of all select document types
List<string> selectedDocumentTypes = new List<string>();
foreach(var item in docTypeFilters.SelectedItems)
{
selectedDocumentTypes.Add(item.ToString());
}
// Create list of all selected file tpyes
List<string> selectedFileTypes = new List<string>();
foreach(var item in fileTypeFilters.SelectedItems)
{
selectedFileTypes.Add(item.ToString());
}
//Cast event item as document object
Document doc = e.Item as Document;
//Apply filter to select discplines and document types
if( doc != null)
{
if (selectedDisciplines.Contains(doc.Discipline) && selectedDocumentTypes.Contains(doc.Type) && selectedFileTypes.Contains(doc.Extension))
{
e.Accepted = true;
} else
{
e.Accepted = false;
}
}
}
There are a couple of problems with your design here.
The way the filter of a collectionview works is it iterates through the collection one by one and returns true/false.
EDIT:
Experimentation seems to confirm this statement is true. AFAIK virtualisation is purely in creation of UI from the collection. Collectionviewsource > Collectionview > Itemssource. Creation of UI rows is virtualised by the virtualising stackpanel but the whole collection will be read into itemssource.
Your filter is complicated and will take a while per item.
It's running 5000 times.
You should not use that approach to filter.
A rethink and fairly substantial refactor is advisable.
Do all your processing and filtering in a Task you run as a background thread.
Forget all that synchronisation context stuff.
Once you've done your processing, return a List of your finalised data back to the UI thread
async Task<List<Document>> GetMyDocumentsAsync
{
// processing filtering and really expensive stuff.
return myListOfDocuments;
}
If that doesn't get edited or sorted then set a List property your itemssource is bound to.
If it does either then new up an observablecollection
YourDocuments = new Observablecollection<Document>(yourReturnedList);
passing your list as a constructor paremeter and set a observablecollection property your itemssource is bound to.
Hence you do ALL your expensive processing on a background thread.
That is returned to the UI thread as a collection.
You set itemssource to that via binding.
The custom observablecollection is a bad idea. You should just use List or Observablecollection where t is a viewmodel. Any viewmodel should implement inotifypropertychanged. Always.
Two caveats.
Minimise the number of rows you present to the UI.
If it's more than a couple of hundred then consider paging and maybe an intermediate cache.
Remove this out your binding
, UpdateSourceTrigger=PropertyChanged
And never use it again until you know what it does.
Some generic datagrid advice:
Avoid column virtualisation.
Minimise the number of columns you bind.
If you can, have fixed column widths.
Consider the simpler listview rather than datagrid.
The problem is your Filter callback. Currently you iterate over three lists inside the event handler (in order to create the filter predicate collections for lookup).
Since the event handler is invoked per item in the filtered collection, this introduces excessive work load for each filtered item.
For example, if each of the three iterations involves 50 items and the filtered collection contains 5,000 items, you execute a total of 5035000 = 750,000 iterations (150 for each event handler invocation).
I recommend to maintain the collections of selected items outside the Filter event handler, so that it doesn't have to be created for each individual item (event handler invocation). The three collections are only updated when a related SelectedItems property has changed.
To further speed up the lookup in the Filter event handler I also recommend to replace the List<T> with a HashSet<T>.
While List.Contains is an O(n) operation, HashSet.Containsis O(1), which can make a huge difference.
You need to track the SelectedItems that are the source for those collections separately to update them.
The following example should speed up your filtering significantly.
/* Define fast O(1) lookup collections */
private HashSet<string> SelectedDisciplines { get; set; }
private HashSet<string> SelectedDocumentTypes { get; set; }
private HashSet<string> SelectedFileTypes { get; set; }
// Could be invoked from a SelectionChanged event handler
// (what ever the source 'disciplineFilters.SelectedItems' is)
private void OnDisciplineSelectedItemsChanged()
=> this.SelectedDisciplines = new HashSet<string>(this.disciplineFilters.SelectedItems.Select(item => item.ToString()));
// Could be invoked from a SelectionChanged event handler
// (what ever the source 'docTypeFilters.SelectedItems' is)
private void OnDocTypeSelectedItemsChanged()
=> this.SelectedDocumentTypes = new HashSet<string>(this.docTypeFilters.SelectedItems.Select(item => item.ToString()));
// Could be invoked from a SelectionChanged event handler
// (what ever the source 'fileTypeFilters.SelectedItems' is)
private void OnFileTypeSelectedItemsChanged()
=> this.SelectedFileTypes = new HashSet<string>(this.fileTypeFilters.SelectedItems.Select(item => item.ToString()));
private void FilterDocuments(object sender, FilterEventArgs e)
{
// Cast event item as document object
if (e.Item is not Document doc) //if (!(e.Item is Document doc))
{
return;
}
// Apply filter to select discplines and document types
e.Accepted = this.SelectedDisciplines.Contains(doc.Discipline)
&& this.SelectedDocumentTypes.Contains(doc.Type)
&& this.SelectedFileTypes.Contains(doc.Extension);
}
Remarks
You should fix your Documents.AddRange method.
It should use the NotifyCollectionChangedAction.Add. NotifyCollectionChangedAction.Replace will trigger the binding target to completely update itself, which is what you want to avoid.
Use the appropriate NotifyCollectionChangedEventArgs constructor overload to send the complete range of added items with the event:
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, list as IList ?? list.ToList()));
Further considerations
Since you said you read the data from a database, you should consider to let the database filter the data for you. Given the correct query, the database will provide far better performance as it is highly optimized for filtering (search queries).
Using the filter feature of the ICollectionView will always block the UI until the collection is filtered. This is because the procedure is not asynchronous. This means you can't display a progress bar as it won't update in real time. Consider to prefilter the items when fetching them from the database. It doesn't make sense to load 5k items when the user can only view 10-50 of them.
If you want to display a progress bar, you better filter the collection directly. This requires a dedicated binding source collection. Since you have already implemented a custom ObservableCollection that exposes a AddRange method, you are good to go (don't forget to fix the CollectionChanged event data).
To add grouping you have to take into consideration that
a) grouping disables row virtualization
b) grouping actually takes place in the UI. The control creates a GroupItem for each group.
To fix a) you need to explicitly enable virtualization while grouping by setting the attached VirtualizingPanel.IsVirtualizingWhenGrouping to true:
<DataGrid VirtualizingPanel.IsVirtualizingWhenGrouping="True" />
To fix b) you could use LINQ grouping, which you could execute on a background thread if necessary:
IEnumerable<IGrouping<string, Document>> groupedDocuments = FilteredItemsSource.DataGridItems.GroupBy(document => document.Author);
dataGrid.ItemsSource = groupedDocuments;
The problem is that DataGrid doesn't know how to display IGrouping. You have to get creative here. Probably extending DataGrid to add this feature would be the best.
If this is not an option, then the only solution I think that is reasonable is to implement data virtualization.
I generally believe that it doesn't make sense to show 5k items at once while the user can only view a fraction.
Just imagine you have 5k items in two groups each of 2.5k items. When the user opens the first, he needs to scroll down 2.5k items before he can see the second group. The UX couldn't get any worse at this point.
If this was my problem to solve, I would reduce the number of items to load. Additionally I would ask myself if the data structure is the correct form to display the data. For example you could create top-down filtering: like first let user select an author, then a created date, etc. Use this filter information to query the database. This should significantly reduce the number of items to display/handle.
Alternatively create an index or use an indexing service like Elastic Search. Such service come with a very advanced query syntax that allow to search/filter the indexed documents more comfortable.
What you are currently doing is not efficient at all and provides a really bad UX.
The following example extends the basic example from above.
You need to bind your DataGrid to the FilteredItemsSource property while you populate the UnfilteredDocuments property with the data from the database.
The example also shows how to replace the CollectionViewSource in XAML with a ICollectionView that is more convenient to handle from C# (code-behind).
it also shows how to gracefully toggle a ProgressBar using the .NET BooleanToVisibilityConverter.
MainWindow.xaml
<Window>
<Window.Resources>
<!-- Use the existing .NET value converter -->
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
</Window.Resources>
<StackPanel>
<ProgressBar IsIndeterminate="True"
Height="4"
Visibility="{Binding ElementName=Window, Path=IsFilterInProgress, Converter={StaticResource BooleanToVisibilityConverter}}" />
<DataGrid ItemsSource="{Binding FilteredItemsSource, Mode=OneTine}"
VirtualizingPanel.IsVirtualizingWhenGrouping="True"
AutoGenerateColumns="False">
</DataGrid>
</StackPanel>
</Window>
*MainWindow.xaml.cs
// The binding source for the ProgressBar.
// Can be bound to Visibility or used as predicate for a Trigger
// This property must be implemented as dependency property!
public bool IsFilterInProgress { get; private set; }
// Binding source for the ItemsControl
public Documents FilteredItemsSource { get; } = new Documents();
// Structure for the database data
private List<Document> UnfilteredDocuments { get; } = new List<Document>();
/* Define fast O(1) lookup collections */
private HashSet<string> SelectedDisciplines { get; set; }
private HashSet<string> SelectedDocumentTypes { get; set; }
private HashSet<string> SelectedFileTypes { get; set; }
private object SyncLock { get; } = new object();
// Constructor
public MainWindow()
{
InitializeComponent();
// Enable CollectionChanged propagation to the UI thread
// when updating a INotifyCollectionChanged collection from a background thread
BindingOperations.EnableCollectionSynchronization(this.FilteredItemsSource, this.SyncLock);
}
// Could be invoked from a SelectionChanged event handler
// (what ever the source 'disciplineFilters.SelectedItems' is)
private async void OnDisciplineSelectedItemsChanged(object sender, EventArgs e)
{
this.SelectedDisciplines = new HashSet<string>(this.disciplineFilters.SelectedItems.Select(item => item.ToString()));
await ApplyDocumentFilterAsync();
}
// Could be invoked from a SelectionChanged event handler
// (what ever the source 'docTypeFilters.SelectedItems' is)
private async void OnDocTypeSelectedItemsChanged(object sender, EventArgs e)
{
this.SelectedDocumentTypes = new HashSet<string>(this.docTypeFilters.SelectedItems.Select(item => item.ToString()));
await ApplyDocumentFilterAsync();
}
// Could be invoked from a SelectionChanged event handler
// (what ever the source 'fileTypeFilters.SelectedItems' is)
private async void OnFileTypeSelectedItemsChanged(object sender, EventArgs e)
{
this.SelectedFileTypes = new HashSet<string>(this.fileTypeFilters.SelectedItems.Select(item => item.ToString()));
await ApplyDocumentFilterAsync();
}
private async Task ApplyDocumentFilterAsync()
{
// Show the ProgressBar
this.IsFilterInProgress = true;
// Allow displaying of a progress bar (prevent the UI from freezing)
await Task.Run(FilterAndSortDocuments);
// Because grouping is actually happening in the UI (by creating GroupItems)
// we can't group on a background thread.
GroupDocuments();
// Hide the ProgressBar
this.IsFilterInProgress = false;
}
// Improve performance by filtering and sorting in one step.
// Use FilterDocuments() if filtering alone (no sorting) is required.
private void FilterAndSortDocuments()
{
IEnumerable<Document> filteredDocuments = GetFilteredDocuments();
// For example sort descending by the property Document.Id
IOrderedEnumerable<Document> filteredAndSortedDocuments = filteredDocuments
.OrderByDescending(document => document.Id);
this.FilteredItemsSource.AddRange(filteredAndSortedDocuments);
}
private void FilterDocuments()
{
this.FilteredItemsSource.Clear();
IEnumerable<Document> filteredDocuments = GetFilteredDocuments();
this.FilteredItemsSource.AddRange(filteredDocuments);
}
private void GroupDocuments()
{
ICollectionView filteredItemsSourceCollectionView = CollectionViewSource.GetDefaultView(this.FilteredItemsSource);
// Allow multiple GroupDescription.Add() and Clear()
// without raising change notifications every time.
// A single change notification is raised after leaving the using scope.
using (var deferredRefreshContext = filteredItemsSourceCollectionView.DeferResfresh())
{
GroupDescriptions groupDescriptions = filteredItemsSourceCollectionView.GroupDescriptions;
groupDescriptions.Clear();
groupDescriptions.Add(new PropertyGroupDescription(nameof(Document.Author)));
}
}
private IEnumerable<Document> GetFilteredDocuments()
{
IEnumerable<Document> filteredDocuments = this.UnfilteredDocuments.Where(IsDocumentAccepted);
return filteredDocuments;
}
private bool IsDocumentAccepted(Document document)
=> this.SelectedDisciplines.Contains(doc.Discipline)
&& this.SelectedDocumentTypes.Contains(doc.Type)
&& this.SelectedFileTypes.Contains(doc.Extension);
using xamarin forms & PCL.
i want to validate the Picker using the Behavior to ensure that user picked an item from the Picker.
my behavior class is
public class PickerValidationBehaviour :Behavior<Picker>
{
private Picker _associatedObject;
public string PropertyName { get; set; }
protected override void OnAttachedTo(Picker bindable)
{
base.OnAttachedTo(bindable);
_associatedObject = bindable;
if (_associatedObject.SelectedIndex < 0 )
{
HandleValidation();
}
}
private void HandleValidation()
{
}
private void _associatedObject_SelectedIndexChanged(object sender, EventArgs e)
{
}
protected override void OnDetachingFrom(Picker bindable)
{
base.OnDetachingFrom(bindable);
_associatedObject = null;
}
}
}
and i was stuck because i want execute the validation before user action, such that the submit button will be hidden until the user fill the form.
and beside if there is any easy efficient way that i can perform the validation please mention it.
I think this scenario you should put logic in VM instead of using behavior.
Cause behavior can change some UI element, like color something and most of them are the element itself's property.
In your case, you want to change another element in Page. There is a problem, how to access another element in your page.
If you binding SelectedIndex in you VM, and when property changed you can raise another property which controls the submit button. That will be easier then do it in behavior.
I have a CollectionViewSource that binds to a custom ObservableDictionary which is in the format of:
ObservableDictionary<int, List<Waypoint>>
A waypoint instance is in a format of:
public class Waypoint
{
public string Name { get; set; }
public float X { get; set; }
public float Y { get; set; }
public float Z { get; set; }
}
Within the same view model, I have another property 'MapId' that is updated as the player moves between maps. I am trying to bind this ObservableDictionary instance to a ListBox to show only the waymaps of a give map based on its mapid. (The int of the dictionary is the map id.)
Is there a way to cause a CollectionViewSource to force-refresh based on another property binding being updated?
I am trying to do this mostly in XAML with as little code-behind as possible.
Here is some of what I have for this currently.
In my view:
<CollectionViewSource x:Key="WaypointCollection" Filter="WaypointCollection_OnFilter" Source="{Binding Waypoints, Source={StaticResource Configurations}}"
/>
In my view code-behind (filter):
private void WaypointCollection_OnFilter(object sender, FilterEventArgs e)
{
var main = SimpleIoc.Default.GetInstance<MainViewModel>();
if (main == null)
{
e.Accepted = false;
return;
}
var waypoint = (KeyValuePair<int, List<Waypoint>>)e.Item;
var zone = main.Player.ZoneId;
e.Accepted = waypoint.Key == zone;
}
You cannot bind. Because they are not DependencyObject(DO). But if this another property is a DO/DP. Then you can force refresh using Refresh method. You can specify a callback to get notified of any changes to this DP, while registering your DP.
... , new PropertyMetadata(new PropertyChangedCallback(MyCallback));
I resorted to creating a service to handle loading things as needed via async tasks. Works as needed although it is not the approach I originally wanted.
So Microsoft has decided to make the process of programmatically selecting items in a TreeView obscenely difficult and malfunctional for some insane reason or other, and the only way to do it (since virtualization ensures that any TreeViewItem you try to select doesn't currently exist) is to create a boolean IsVisible property on whatever you want to use for your source data, which by the way has to implement INotifyPropertyChanged and include an event handler now if it didn't before, and then add an ItemContainerStyle to your TreeView binding the property to the IsVisible property of the TreeViewItem.
What this doesn't do however, is set focus to the selected item, so if for example your goal was to let the user delete tree items with the keyboard and have the focus automatically shift to the deleted item's parent so the user doesn't have to continuously tab through items or click on things to get their place back, this is nearly useless. It somehow doesn't even avoid the virtualization problem, allowing you to set the selection to something that it claims in the next line of code doesn't exist.
Here's my XAML:
and relevant C#:
public partial class MainWindow : Window
{
public ObservableCollection<TreeEdit> Source { get; set; }
public MainWindow()
{
Source = new ObservableCollection<TreeEdit>();
Source.Add(new TreeEdit("Hi"));
DataContext = this;
InitializeComponent();
}
private void KeyNav(object sender, KeyEventArgs e)
{
TreeEdit Selection = (sender as TreeView).SelectedItem as TreeEdit;
ObservableCollection<TreeEdit> TSource = (ObservableCollection<TreeEdit>)(sender as TreeView).ItemsSource;
if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
{
if (e.Key == Key.Left && Selection != null)
{
if (Selection.Parent == null)
{
TSource.Remove(Selection);
}
else
{
Selection.Parent.IsSelected = true;
((TreeViewItem)((sender as TreeView).ItemContainerGenerator.ContainerFromItem(Selection.Parent))).Focus();
Selection.Parent.Remove(Selection);
}
}
}
}
}
public class TagEdit : INotifyPropertyChanged
{
public string Name
{
get
{
return name;
}
set
{
OnPropertyChanged("Name");
name = value;
}
}
private string name;
public bool IsSelected
{
get
{
return selected;
}
set
{
OnPropertyChanged("IsSelected");
selected = value;
}
}
private bool selected;
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
public TagEdit Parent;
public ObservableCollection<TagEdit> Children { get; set; }
public TagEdit(string n)
{
Name = n;
Children = new ObservableCollection<TagEdit>();
}
public void Remove(TagEdit tag)
{
Children.Remove(tag);
}
}
The idea is that the user can navigate the TreeView normally with the arrow keys, then use Ctrl+Left to delete the selected item and select its parent. (sender as TreeView).ItemContainerGenerator.ContainerFromItem(Selection.Parent) often returns null. Removing it causes the proper item to be selected, but the TreeView loses focus. When it doesn't return null, I get the expected behavior.
Also, despite both the TreeView's KeyboardNavigation.TabNavigation and KeyboardNavigation.ControlTabNavigation being set to the default value of Continue, they behave differently (Tab ignores the TreeView's component elements while Ctrl+Tab steps into them).
I've been trying to get something, anything to work for almost a week now and if my opening paragraph didn't tip you off I've long since worn out my patience. Please don't tell me to try anything unless you have already, personally typed exactly what you're about to tell me to try into VisualStudio and it worked.
Apologies for the harsh tone, but this problem went beyond ridiculous and into obscene some time ago.
The WPF TreeView doesn't have a single ItemContainerGenerator. Every item in a tree view is an ItemsControl and thus has its own ItemContainerGenerator for its child items. What you really need to do is to get the grand parent of the item you are going to delete and use THAT ItemContainerGenerator to call ContainerFromItem.
It's not working because you are trying to use the top level ItemContainerGenerator which only contains the top level items.
P.S. on a friendly side note :), who deletes items with a Ctrl+Left? That's undiscoverable. Why not just do this behavior when they hit Delete?
Typically, when I use the standard TreeView control that comes with C#/VB I write my own methods to transfer data in and out of the Tree's internal hierarchy store.
There might be ways to "bind" the GUI to a data store that I can point to (such as XML files), and when the user edits the tree items, it should save it back into the store. Is there any way to do this?
I got around it by creating a class that Inherits TreeNode and contains an object.
you can then bind a record to the node and recall it during the Click or DoubleClick event.
Eg.
class TreeViewRecord:TreeNode
{
private object DataBoundObject { get; set; }
public TreeViewRecord(string value,object dataBoundObject)
{
if (dataBoundObject != null) DataBoundObject = dataBoundObject;
Text = value;
Name = value;
DataBoundObject = dataBoundObject;
}
public TreeViewRecord()
{
}
public object GetDataboundObject()
{
return DataBoundObject;
}
}
then you can bind to each node as you build your TreeView eg.
TreeView.Nodes.Add(new TreeViewRecord("Node Text", BoundObject));
//or for subNode
TreeView.Nodes[x].Nodes.Add(new TreeViewRecord("Node Text", BoundObject));
Then you can bind the DoubleClick event to something like this
private void TreeViewDoubleClick(object sender, TreeNodeMouseClickEventArgs e)
{
object exp = ((TreeViewRecord) e.Node).GetDataboundObject();
//Do work
}