Execute custom code after command was executed - wpf

I have a TreeView control, and after a new node is added, I need to execute some custom code (ex expand the node, make it visible, and fire begind edit method). Since I want this to be available in every instance of TreeView control, I thought of subclassing the TreeView control.
Now, in order for treeview to know when the new node is added, it would either have an event that is fired when item is added (which it doesn't), or to have a reference to command that was executed to add a new item.
So two questions:
1) Is there a way to add an event in TreeView that would be fired whenever a treenode is added (I am always adding nodes through source collection from ViewModel) - I could not find any way to do this
2) I could add an AddCommand property to TreeList, that would be bound to ViewModel's AddCommand, and then have some button, or ContextMenu item that would bind to TreeList.AddCommand, instead of view model. This way TreeView would hold reference to AddCommand, but the drawback would be that actual usage would be kind of wierd. Question: How can I know when an TreeView's AddCommand (or any command, to that matter) is executed, so I can fire some custom code after it? It seems that CommandManager.AddExecutedHandler is a solution, but I am unable to execute it.

Is this any help?
public class CustomTreeControl : TreeView
{
...
// WPF only
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{
base.OnItemsSourceChanged(oldValue, newValue);
Debug.WriteLine("OnItemsSourceChanged");
}
// WPF + Silverlight
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);
Debug.WriteLine("OnItemsChanged: {0}", e.Action);
}
}

Related

Master-detail: How to fetch a control from a template inside the "detail" ContentControl?

I have a ListView (on the 'master' side) whose selection drives a ContentControl's Content property (on the 'detail' side). The ContentControl's visual tree comes from either of two DataTemplate resources that use DataType to choose which detail view to render based on what is selected in the ListView.
That part works fine.
The part I'm struggling with is that there is a particular control inside (one of) the templates that I need to obtain a reference to whenever it changes (e.g. the template selected changes or the ListView selection changes such that the instance of the control is recreated.)
In my ListView.SelectionChanged event handler, I find the ContentControl has not yet been updated with its new visual tree, so initially it's empty on the first selection, and for subsequent selections its visual tree matches the old selection instead of the new one.
I've tried delaying my code by scheduling on the Dispatcher with a priority as low as DispatcherPriority.Loaded, which works for the first selection but on subsequent selections my code still runs before the visual tree is updated.
Is there a better event I should be hooking to run whenever the ContentControl's visual tree is changed to reflect a changed data-bound value to its Content property?
Extra info: the reason I need to reach into the expanded DataTemplate is that I need to effectively set my view model's IList SelectedItems property to a DataGrid control's SelectedItems property. Since DataGrid.SelectedItems is not a dependency property, I have to do this manually in code.
The fix required a combination of techniques. For the first selection that populates the visual tree, I needed to handle ContentControl.OnApplyTemplate() which is only a virtual method rather than an event. I derived from it and exposed it as an event:
public class ContentControlWithEvents : ContentControl
{
public event EventHandler? TemplateApplied;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
this.TemplateApplied?.Invoke(this, EventArgs.Empty);
}
}
In the XAML I used the above class rather than ContentControl:
<local:ContentControlWithEvents
Content="{Binding SelectedAccount}"
x:Name="BankingSelectedAccountPresenter"
TemplateApplied="BankingSelectedAccountPresenter_TemplateApplied" />
Then I handle the event like this:
void BankingSelectedAccountPresenter_TemplateApplied(object sender, EventArgs e) => this.UpdateSelectedTransactions();
private void UpdateSelectedTransactions()
{
if (this.MyListView.SelectedItem?.GetType() is Type type)
{
DataTemplateKey key = new(type);
var accountTemplate = (DataTemplate?)this.FindResource(key);
Assumes.NotNull(accountTemplate);
if (VisualTreeHelper.GetChildrenCount(this.BankingSelectedAccountPresenter) > 0)
{
ContentPresenter? presenter = VisualTreeHelper.GetChild(this.BankingSelectedAccountPresenter, 0) as ContentPresenter;
Assumes.NotNull(presenter);
presenter.ApplyTemplate();
var transactionDataGrid = (DataGrid?)accountTemplate.FindName("TransactionDataGrid", presenter);
this.ViewModel.Document.SelectedTransactions = transactionDataGrid?.SelectedItems;
}
}
}
Note the GetChildrenCount check that avoids an exception thrown from GetChild later if there are no children yet. We'll need that for later.
The TemplateApplied event is raised only once -- when the ContentControl is first given its ContentPresenter child. We still the UpdateSelectedTransactions method to run when the ListView in the 'master' part of the view changes selection:
void BankingPanelAccountList_SelectionChanged(object sender, SelectionChangedEventArgs e) => this.UpdateSelectedTransactions();
On initial startup, SelectionChanged is raised first, and we skip this one with the GetChildrenCount check. Then TemplateApplied is raised and we use the current selection to find the right template and search for the control we need. Later when the selection changes, the first event is raised again and re-triggers our logic.
The last trick is we must call ContentPresenter.ApplyTemplate() to force the template selection to be updated before we search for the child control. Without that, this code may still run before the template is updated based on the type of item selected in the ListView.

How do I get the UIElement when Collection changes?

I have a wpf Treeview which has a dynamic itemssource. The User can add and remove items at runtime.
I'm missing an event which gives me the currently added UIElement that was added to the treeviews itemsSource. So I guess I need to switch to OnCollectionChanged.
This is what I have:
// MyItemViewModel is a viewmodel for a TreeViewItem
// MyCollection is bound to hte Treeview's ItemsSource
public class MyCollection : ObservableCollection<MyItemViewModel>
{
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
// i like to have the UIelement which was added to the collection
// (e.NewItems contains the FrameworkElement's DataContext)
break;
}
}
}
Im following MVVM, as good as I can, and don't want to hold any view elements in the viewmodel.
I like to have an event that is fired when an item is added, which provides the new added UIElement in its sender or EventArgs.
I already tried ItemContainerGenerator class, but it's not useful inside a viewmodel since it requires already a UIElement Control.
You seem to be looking at this problem from the wrong direction... in MVVM, you can pretty much forget about the UI for the most part. So, instead of thinking how to get hold of the item that the user added into the collection control in the UI, think about accessing the data object that you added to the data collection in the view model that is data bound to the UI collection control in response to an ICommand that was initiated by the user.
So to me, it sounds like you need to implement an ICommand that is connected to a Button in the UI, where you add the new item into the data bound collection rather than any event. In this way, you'll always know the state of all of your data items.

Expand Root in TreeView when ItemSource changed

I have a ChildWindow in a Silverlight 4 App with a TreeView. The ItemSource is binded to an ObservableCollection of Items in a ViewModel. When the window opens the item are loaded from a webservice.
I have only one root node and I need it to be initially expanded. The TreeView even has the extension ExpandToDepth() which seems perfect but I don't know where I can call it. I didn't find an event that occurs after the items are updated from the ItemSource.
I tried using ItemContainerGenerator.ItemChanged and OnItemsChanged in TreeView but they are both executed before the tree view items are generated so the expand commands won't work.
How can I get this to work?
Try to register for your ObservableCollection's CollectionChange Event and do your actions there.
Derive from TreeView and create IsRootItemExpanded property and create same name property in ViewModel. In set accesser check if value true then call your method like this ExpandToDepth(1) When itemssource is need to update then set IsRootItemExpanded property true. You must to bind IsRootItemExpanded of TreeView to IsRootItemExpanded of ViewModel.
I solved my problem by overriding the PrepareContainerForItemOverride method in TreeView. I wondered why this is just called for the root node but it works.
public class ExpandedRootTreeView : TreeView
{
protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
{
TreeViewItem treeViewItem = element as TreeViewItem;
if (treeViewItem != null) treeViewItem.IsExpanded = true;
base.PrepareContainerForItemOverride(element, item);
}
}
Thanks to everybody who responded.

WPF ItemsControl get container from data object (TreeView, Multiselect)

How can I get the Container for an object in WPF ItemsControl.
I am writing a multiselect treeview with bindable SelectedItem und SelectedItems Dependency Properties. So long everything works just fine. The only thing is, when I click on an item in the tree with pressed ctrl a second time this item should not be selected but the last previous selected item. The TreeView contains a private Method called ChangeSelection. As far as i understand the first parameter is the Container, the second is the TreeViewItem and the last wherether the item shall be selected or not.
I implement the multiselection with catching the SelectedItemChanged event.
This code works for the new selected item
private void OnSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
var view = ItemContainerGenerator.ContainerFromItem(e.NewValue) as TreeViewItem;
// ...
}
BUT if i want to get the TreeViewItem from an item saved in an ObservableCollection... it will not work.
EDIT: Ok, as i found out. The code above works only for the first level of items...
EDIT: The solution for this problem isn't trivial. It is possible to find the selected treeview item by using a viewmodel (f.e. an interface which provides basics like: IsSelected, IsExpanded, IsEnabled and Parent). You can search the TreeViewItem like this:
if (treeViewItem.ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated)
{
EventHandler eventHandler = null;
eventHandler = delegate
{
treeViewItem.ItemContainerGenerator.StatusChanged -= eventHandler;
// Call the search function recursive XYZ(tree, treeViewItem.ItemContainerGenerator.ContainerFromItem(nextLevelItem) as TreeViewItem);
};
// wait for the containers to be generated
treeViewItem.ItemContainerGenerator.StatusChanged += eventHandler;
}
else
{
// Call the search function recursive XYZ(tree, treeViewItem.ItemContainerGenerator.ContainerFromItem(nextLevelItem) as TreeViewItem);
}

Event handler that will be called when an item is added in a listbox

Is there an event handler that will be called when an item is added in a listbox in WPF?
Thanks!
The problem is that the INotifyCollectionChanged interface which contains the event handler is explicitly implemented, which means you have to first cast the ItemCollection before the event handler can be used:
public MyWindow()
{
InitializeComponent();
((INotifyCollectionChanged)mListBox.Items).CollectionChanged +=
mListBox_CollectionChanged;
}
private void mListBox_CollectionChanged(object sender,
NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
// scroll the new item into view
mListBox.ScrollIntoView(e.NewItems[0]);
}
}
Ref.
Josh's advice about the observable collection should also be considered.
Take a different approach. Create an ObservableCollection (which does have such an event) and set the ItemsSource of the ListBox to this collection. In other words, in WPF you should think about the problem differently. The control isn't necessarily what is being modified ... the collection behind it is.
UPDATE
Based on your comment to Mitch's answer which indicates your binding source is actually an XML document, I suggest looking into hooking up to the XObject.Changed event of the XML document/element/etc. This will give you change information about the XML structure itself - not the ItemCollection which is an implementation detail you shouldn't need to consider. For example, ItemCollection (or any INotifyCollectionChanged) doesn't guarantee an individual event for every change. As you noted, sometimes you'll just get a generic reset notification.

Resources