I have an ItemsControl displaying a list via binding. The list is of a ViewModel type which is then referenced in a DataTemplate to display a button. The button has it's UID bound to a GUID from the the view model. When a new view model is added to the bound list, I need to get hold of the button that will be added, as the buttons need to be able to be dragged/dropped by the user. At the moment the closest I can get is finding the ContentPresenter that displays the button, but the content of that ContentPresenter is of type view model.
Is there a way to find the button that has been added? Or should I not used a DataTemplate and create the buttons my self in order to access them?
I have used the VisualTree helper to get the content presenter, but have not managed to find the button.
You could handle the Loaded event for the Button:
<DataTemplate>
<Button Loaded="OnButtonLoaded" ... />
private void OnButtonLoaded(object sender, RoutedEventArgs e)
{
Button button = (Button)sender;
//...
}
You won't be able to get a reference to it using the VisualTreeHelper until it has actually been added to the visual tree and loaded anyway.
Related
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.
So i am build simple Clipboard manager.
Every ListViewItem come from Clipboard.GetText and my application minimize to Tray and when double click on its Icon the application jump and i want to focus become on the first ListViewItem in order to be able to navigate with Up & Down arrows.
This is my ListView:
ListView myListView;
Model List:
public ObservableCollection<string> Clipboards
Window Loaded event:
private void Window_Loaded(object sender, RoutedEventArgs e)
{
if (viewModel.Clipboards.Count != 0)
myListView.ScrollIntoView(myListView.Items[0]);
myListView.Focus();
}
So currently when i open int the first time the application, the Focus is not on any ListViewItem and in the next time the focus is on the last selected/click ListViewItem and not on the first one.
It looks like you have your ViewModel in the code behind. This is not good MVVM standard.
Maybe this could be helpful
How can I set the focus to a ListBox properly on load if it uses databinding?
From what I see you are not focusing any ListViewItem but the ListView itself. I think this is your mistake. To focus the item you have to get it's container. The objects in the ItemsSource are actually the data itself and no the UIElement to render. To draw this data or add it to the visual tree for rendering, the ItemsControl will generate a container for the data e.g. a ListViewItem. Only the UIElement can receive focus, that's why the UIElement exposes the Focus() method. You have to use the ItemContainerGenarator to retrieve this container for your data:
ListView myListView;
(myListView.ItemsPanel as VirtualizingPanel)?.BringIndexIntoViewPublic(0);
myListView.Dispatcher.Invoke(new Action(
() =>
{
ListBoxItem dataContainer = myListView.ItemContainerGenerator.ContainerFromIndex(0) as ListBoxItem;
dataContainer?.Focus();
}), DispatcherPriority.ContextIdle);
This example will move the focus to the first element in the ListView.
One of those 'Why is this so hard?" questions.
I have a ListBox (containing details of share portfolios). The listbox item uses a grid to display attributes of the portfolio. Source is a list of portfolios in the View Model.
ListBox is multiselect - when selection changes, a list of the constituents of the selected portfolios is re-populated.
What I want to do is put a button (or menu or whatever) on the listboxitem to display a list of possible actions (Trade, Unitise, Delete etc).
When an action is selected I need to execute the action against the appropriate portfolio. Ideally I want the actions to be available for both selected and unselected items.
I can handle the event, but how do I detect which item (portfolio) the user selected? I've looked at GotFocus() but it doesn't seem to fire.
In other words if a control in a Listboxitem, fires an event, how does the event 'know' which ListBoxItem raised it?
For me, the solution here, seen as you mentioned MVVM, would be to have the ListBox populated by a collection of ViewModels, e.g., something like ObservableCollection<PortfolioViewModel>.
It would then just be a case of binding the Command property of the Button to an ICommand on the ViewModel that executes whatever work you need doing.
I can handle the event, but how do I detect which item (portfolio) the user selected? I've looked at GotFocus() but it doesn't seem to fire.
You could cast the DataContext of the clicked Button to the corresponding object in te ListBox, e.g.:
private void DeleteButton_Clicked(object sender, RoutedEventArgs e)
{
Button deleteButton = sender as Button;
var portfolio = deleteButton.DataContext as Portfolio; //or whatever your type is called
//access any members of the portfolio...
}
I have a listbox with several controls. Each control contains an custom autocompletebox which contains a System.Windows.Controls.AutoCompleteBox. When I right click on the control a custom contextm menu shows up. But with a right click on the textbox there appears the default contextmenu of the TextBox (with Copy, Cut and Paste).
My goal is to show my custom context menu with a right click on the TextBox.
Further informations:
My custom context menu is defined in the DataTemplate of the ListBox but I could define it in the Ressources or somewhere else as well.
I tried:
- when I null the context menu of the custom autocompletebox or the System.Windows.Controls.AutoCompleteBox of it, there is no effect at all
Thanks for every help ;)
You can either bind the context menu property to parent element's context menu or bind to the context menu once you define in the resource.xaml
Try using PreviewMouseDown instead of MouseDown to handle the MouseDown event.
In XAML:
<ListBox Margin="3" PreviewMouseDown="MouseDownOnListBox">
In Code:
private void MouseDownOnListBox(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Right)
{
//Display your context menu
}
}
If you are using PreviewMouseDown on a list, when you click anywhere on the list, this event will be triggered first.
In this question I asked about adding TabItems dynamically to a TabControl
the ItemsSource are from ObservableCollection<Village>..
My question is if added a button to any TabItem, this button will have the DataContext of its container TabItem, how could I implement the Click event for this button?
If you have added the Button to the DataTemplate, then on your Button_Click method you can easily get the 'Village' datacontext.
void Button_Click(object sender, RoutedEventArgs e)
{
Village clickedVillage = ((Button)sender).DataContext as Village;
//Do whatever you want to do with the Village
}
But again, the above solution is not the best way to go for this problem. MVVM pattern would expect a ICommand in your Village (Or its container class) and you will bind that command to the Button.Command property so there wont be any code-behind at all. Or in other words your XAML will be more cleaner and ViewModel will get more self-contained in terms of properties and actions.