WPF: Get object visualized by (hierarchical) data template - wpf

I've got a TreeView with a couple of items in it. The items are visualized by a simple hierarchical data template, like this:
<HierarchicalDataTemplate x:Key="instanceTemplate">
<CheckBox Checked="InstanceCheckChanged" Unchecked="InstanceCheckChanged">
<Label>Hello World!</Label>
</CheckBox>
</HierarchicalDataTemplate>
As you can see I've added an event handler, here's the code behind:
private void InstanceCheckChanged(object sender, RoutedEventArgs e)
{
CheckBox checkBox = (CheckBox)sender;
}
In this event handler, the sender of the event is obviously the check box itself, however the checkbox is actually visualizing my normal object. My question is, how do I get the object that the checkbox visualized? Preferably I would like to have a method with a signature like this:
public MyObject GetMyObject(UIElement sender);
Is this possible in WPF, or is there a clean way to store some metadata so that I know which MyObject was checked?

Your CheckBox's DataContext will be the object that it's representing:
var myObject = ((CheckBox)sender).DataContext as MyObject;

Related

InvalidCastException in ComboBox SelectionChanged event handler

This code works well:
private void Combobox1_Loaded(object sender, RoutedEventArgs e)
{
var combo = (ComboBox)sender;
var pointGroupList = (List<PointGroup>)combo.ItemsSource;
combo.ItemsSource = pointGroupList.Select(group => group.Name);
}
But this one doesn't work at all:
private void Combobox1_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var combo = (ComboBox)sender;
var pointGroupList = (List<PointGroup>)combo.ItemsSource;
textBlock1.Text = "num of points:" + pointGroupList.Find(group => group.Name == (string)combo.SelectedItem).PointsCount.ToString();
}
Here's the message in my output window:
System.InvalidCastException: Unable to cast object of type 'WhereSelectListIterator2[Autodesk.Civil.DatabaseServices.PointGroup,System.String]' to type 'System.Collections.Generic.List1[Autodesk.Civil.DatabaseServices.PointGroup]'. at _01_COGO_Points.ModalDialog_1.Combobox1_SelectionChanged(Object sender, SelectionChangedEventArgs e) in D:\00 Materials\c3d\c#\examples\ACAD\01 COGO Points\Window.xaml.cs:line 49
Any help will be appreciated.
What you're doing in your Loaded event is quite weird. I would not recommend doing that, as it will wreck your bindings. If the reason you're doing that is so that the Name property is shown in your ComboBox, you should rather use a DataTemplate. Something like this:
<Window.Resources>
<DataTemplate x:Key="pntGroupTemplate" DataType="{x:Type ac:PointGroup}">
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</Window.Resources>
and you will of course need to add a namespace to your Window. Something like this:
xmlns:ac="clr-namespace:Autodesk.Civil.DatabaseServices;assembly=AeccDbMgd"
I don't have Civil, so not sure if that's exactly right, but it should be close. Intellisense should be able to help you with the correct path if this one is not quite right.
and in your combobox,
<ComboBox ItemTemplate="{StaticResource pntGroupTemplate}" ... />
My best advice is to just completely get rid of all the code in your Combobox1_Loaded event handler and create a DataTemplate in xaml to display the Name property using the code snippet above. And lastly, change your lambda expression from this:
group => group.Name == (string)combo.SelectedItem
to this:
group => group.Name == (combo.SelectedItem as PointGroup)?.Name
The exception you're getting is due to the second line. When you call the Select method in the Loaded event, it returns IEnumerable<string>, so when you're casting the ItemsSource to List<PointGroup> that's where everything goes sideways in so many different ways :-).
Another issue with what you're doing, is that now, SelectedItem is a string, and does not have a Name property.
Hope that helps

TextBlock binding to selected DataGrid Element

Does anyone know how to dynamically set the ElementName for textblock.text binding?
I have two Datagrids that have the same information but the second DataGrid is just a filter of the same datasource, but what I want is to bind text of a textblock to the selected item depending if the item was clicked in the main datagrid or the secondary datagrid.
I have the below code to bind the textblock to one datagrid but I would also like the same to happen if the user clicked an item in the secondDataGrid.
Is this possible?
<TextBlock Margin="29,0" Text="{Binding SelectedItem.Name, ElementName=MainDataGrid}"
It is possible, though I don't think this is the proper solution.
You can handle one of the DataGrids' events in the code-behind, where in the handler you can write the following code:
BindingOperations.SetBinding(textBlock, TextBlock.TextProperty,
new Binding("SelectedItem.Name")
{
ElementName = "DataGrid1"
});
Basically you reset the Binding on the TextBlock's Text property with this code, where:
textBlock is the name of your TextBlock;
with TextBlock.TextProperty you define that you want to work with the Text property on the TextBlock;
The third paramter is the new Binding itself. The constructor takes the Path of the Binding and then in the "body" I set the ElementName.
If DataGrid1 fires the event you set the ElementName to that DataGrid's name, if DataGrid2 fires the event then you set the ElementName to the second DataGrid's name.
SelectionChanged can be a good event to handle on both DataGrid, but if you want the TextBlock to update when you selected and element in the first then select another one in the second and then click back to the first element to update then you need to handle the GotFocus event as well.
Play a little bit with it and you will see what I mean.
My working example:
private void SetBindingOnTextBlock(string elementName)
{
BindingOperations.SetBinding(textBlock, TextBlock.TextProperty, new Binding("SelectedItem.Name")
{
ElementName = elementName
});
}
private void DataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
SetBindingOnTextBlock("DataGrid1");
}
private void DataGrid_SelectionChanged_1(object sender, SelectionChangedEventArgs e)
{
SetBindingOnTextBlock("DataGrid2");
}
private void DataGrid1_GotFocus(object sender, RoutedEventArgs e)
{
SetBindingOnTextBlock("DataGrid1");
}
private void DataGrid2_GotFocus(object sender, RoutedEventArgs e)
{
SetBindingOnTextBlock("DataGrid2");
}
UPDATE 1:
Set
IsSynchronizedWithCurrentItem="True"
on the DataGrids and it may solve your problem if ItemsSources are the same. (Not sure if this is what #dkozl meant) Originally I assumed that they are different.

Binding Button.IsEnabled to position of current in CollectionView

I am trying to bind the IsEnabled property of a button to properties of the window's CollectionViewSource. I am doing this to implement First/Previous/Next/Last buttons and want the First and Previous to be disabled when the view is on the first item etc.
I have the collection view source set up, UI controls binding to it correctly, with access to its view in code so the click event handlers work fine in navigating through the view.
<CollectionViewSource x:Key="cvMain" />
The DockPanel is the root element of the window
<DockPanel DataContext="{StaticResource cvMain}">
FoJobs is an observable collection, cvJobs is a CollectionView that I use in the button's click handler
private void Window_Loaded(object sender, RoutedEventArgs e) {
((CollectionViewSource)Resources["cvMain"]).Source = FoJobs;
cvJobs = (CollectionView)((CollectionViewSource)Resources["cvMain"]).View;
}
I have tried this but get a binding error "BindingExpression path error: '' property not found on 'object' ''ListCollectionView'"
<Button Name="cbFirst" Click="cbMove_Click" IsEnabled="{Binding Source={StaticResource cvMain}, Converter={StaticResource CurrPos2BoolConverter}}" />
I am trying to do with a converter first but figure a style with triggers would be more efficient, but cant get access to the collection view. Even though the underlying datacontext is set to a collection view source, the binding is passed to the converter as the view's source (if I dont explicity set the binding's Source, as above), which has no currency properties (CurrentPosition, Count etc).
Any help would be greatly appreciated.
Why don't you use a RoutedCommand for this(even if you don't use MVVM that is)?
say something like:
<Button x:Name="nextButton"
Command="{x:Static local:MainWindow.nextButtonCommand}"
Content="Next Button" />
and in your code-behind:
public static RoutedCommand nextButtonCommand = new RoutedCommand();
public MainWindow() {
InitializeComponent();
CommandBinding customCommandBinding = new CommandBinding(
nextButtonCommand, ExecuteNextButton, CanExecuteNextButton);
nextButton.CommandBindings.Add(customCommandBinding); // You can attach it to a top level element if you wish say the window itself
}
private void CanExecuteNextButton(object sender, CanExecuteRoutedEventArgs e) {
e.CanExecute = /* Set to true or false based on if you want button enabled or not */
}
private void ExecuteNextButton(object sender, ExecutedRoutedEventArgs e) {
/* Move code from your next button click handler in here */
}
You can also apply one of the suggestions from Explicitly raise CanExecuteChanged() to manually re-evaluate Button.isEnabled state.
This way your encapsulating logic relating to the button in one area.

Object reference not set to an instance of an object when creating two ComboboxItems functions in wpf

I´m all out of ideas here
The thing is that Im using two comboboxes and I want to get values from both comboboxes to show content in DataGrid in wpf.
I have this function that gets values from both comboboxes. This works well.
private void cboxYearChange(object sender, SelectionChangedEventArgs e)
{
ComboBoxItem typeItemYear = (ComboBoxItem)comboBox2.SelectedItem;
string valueYear = typeItemYear.Content.ToString();
ComboBoxItem typeItemMonth = (ComboBoxItem)comboBox1.SelectedItem;
string valueMonth = typeItemMonth.Content.ToString();
}
But then I want to create another function to check for changes on the other combobox:
private void cboxMonthChange(object sender, SelectionChangedEventArgs e)
{
ComboBoxItem typeItemYear = (ComboBoxItem)comboBox2.SelectedItem;
string valueYear = typeItemYear.Content.ToString();
ComboBoxItem typeItemMonth = (ComboBoxItem)comboBox1.SelectedItem;
string valueMonth = typeItemMonth.Content.ToString();
}
I can build, but when I run this I get the Object reference not set to an instance of an object error on the ComboBoxItem typeItemYear = (ComboBoxItem)comboBox2.SelectedItem; line in the cboxMonthChange function
What am I missing here ?
SelectedItem is null until something is selected. Unless they both change at the same time (which is not possible as these events are fired in sequence), either the type cast on comboBox1.SelectedItem or comboBox2.SelectedItem will throw an exception.
Check if SelectedItem is set the methods.
Or use another cast, like:
ComboBoxItem item1 = comboBox1.SelectedItem as ComboBoxItem;
if (item1 != null)
{
// do something
}
Hope this helps :-)
1) you should not refer to control's name within the code whenever possible.
So you can know, for instance, which ComboBox was changed within a SelectionChanged
handler by casting the Sender to a ComboBox.
2) but in such a simple case, just use public properties and bind them to
your ComboBox : all will get done with no code.
<ComboBox x:Name="YearSelectCB" SelectedItem="{Binding SelectedYear}">
<ComboBox x:Name="MonthSelectCB" SelectedItem="{Binding SelectedMonth}">
(you can set the DataContext of the window in several ways, for instance in the
window loaded event handler (DataContext=this) )

WPF expand TreeView on single mouse click

I have a WPF TreeView with a HierarchicalDataTemplate.
Currently I have to double click an item to expand/collapse it.
I would like to change this behaviour to a single click, without loosing other functionality. So it should expand and collapse on click.
What is the recommended way to do this?
Thanks!
You could use a re-templated checkbox as your node (containing whatever template you are currently using) with its IsChecked property bound to the IsExpanded property of the TreeViewItem.
Here is a template I've just test that seems to do the job:
<HierarchicalDataTemplate ItemsSource="{Binding Items}">
<CheckBox IsChecked="{Binding RelativeSource={RelativeSource AncestorType=TreeViewItem}, Path=IsExpanded}">
<CheckBox.Template>
<ControlTemplate>
<TextBlock Text="{Binding Header}"></TextBlock>
</ControlTemplate>
</CheckBox.Template>
</CheckBox>
</HierarchicalDataTemplate>
Just replace the ControlTemplate contents with whatever you need.
If you are using a standard TreeViewItem, then you can capture the click event:
private void OnTreeViewMouseUp( object sender, MouseButtonEventArgs e )
{
var tv = sender as TreeView;
var item = tv.SelectedItem as TreeViewItem;
if( item != null )
item.IsExpanded = !item.IsExpanded;
e.Handled = true;
}
private void OnTreeViewPreviewMouseDoubleClick( object sender, MouseButtonEventArgs e )
{
e.Handled = true;
}
Most likely in your case, you'll need to do something with your binding and ViewModel. Here's a good article from CodePlex: Simplifying the WPF TreeView by Using the ViewModel Pattern.
Just use selected item changed event and use the following,
private void treeview_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
TreeViewItem item = (TreeViewItem)treeview.SelectedItem;
item.IsExpanded = true;
}
where treeview is the name of your TreeView, you could include an if to close/open based on its current state.
I have very little experience working with WPF to this point, so I am not 100% certain here. However, you might check out the .HitTest method of both the Treeview and TreeView Item (the WPF Treeview is essentially the Windows.Controls.Treeview, yes? Or a derivation thereof?).
THe HIt Test method does not always automatically appear in the Intellisense menu for a standard Windows.Forms.Treeview (I am using VS 2008) until you type most of the method name. But it should be there. You may have to experimnt.
You can use the .HitTest Method to handle the MouseDown event and return a reference to the selected treeview item. You must test for a null return, however, in case the use clicks in an area of the control which contains no Tree Items. Once you have a reference to a specific item, you should be able to set its .expanded property to the inverse of whatever it is currently. again, some experimentation may be necessary here.
As I said, I have not actually used WPF yet, so I could have this Wrong . . .
The answer of Metro Smurf (thanks to which I got where I wanted to be) suggests the right approach . You could simply hook up to the SelectedItemChanged event of the Treeview. Then cast the e.NewValue passed in the eventhandler as TreeViewItem, and access its IsExpanded property to set it to true.
void MyFavoritesTreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
((TreeViewItem)e.NewValue).IsExpanded = true;
}
Then for the final touch, you can also hook up the items in your Treeview by casting them as TreeViewItem as suggested, and then you can hook up to the various manipulation events, like:
var item = tv.SelectedItem as TreeViewItem;
item.Expanded += item_Expanded;
And then do whatever you need to do in the eventhandler
void item_Expanded(object sender, RoutedEventArgs e)
{
// handle your stuff
}

Resources