WPF Access datagrid in tabs - wpf

I have a templated tabcontrol with templated datagrids in each tab, like this:
<TabControl>
<TabControl.ItemTemplate >
<DataTemplate>
....
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<DataGrid>
<DataGrid.ColumnHeaderStyle>
...
</DataGrid.ColumnHeaderStyle>
<DataGrid.CellStyle>
...
</DataGrid.CellStyle>
</DataGrid>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
I omitted data bindings in the code so it reads better.
I want to specify a tab, and a cell in the datagrid (row and column) and after clicking a button make the program open the specified tag and scroll down through the table to highlight the specified cell automatically, similar to what is done in visual studio when you click on a compile error and it takes you to the line in the file where the error is.
I am changing the selected tab with Tab.SelectedIndex, and that works, but I can't access the datagrid inside of the tab because it is only generated when the tab is clicked manually. I tried using Load but it doesn't work. How can I generate and access the datagrid inside each tab?

Is it because it's being loaded asynchronously? Try using performing your grid selection after loading using the Loaded event.
TabItem selectedTab = MyTabControl.SelectedItem as TabItem;
selectedTab.Loaded+= delegate { //select grid row };
//or if this works, I'm sure it would have been initialized in the process of being selected?
TabItem selectedTab = MyTabControl.SelectedItem as TabItem;
selectedTab.MyGrid...
Or maybe selectedTab.Initialized.
The tab items are also children of the tabControl, so you should be able to access them using Children[index] as TabItem

Related

How to have dynamically loaded instances of unbound named XAML elements behave independently from each other?

I have a WPF application where I dynamically load document view instances into a TabControl. The view has a ToolBar with some ToggleButtons which I use to control the visibility of certain elements in that view like so (only relevant elements shown):
<UserControl x:Class="MyProject.View.Views.DocumentView" ...>
...
<ToolBar>
<ToggleButton x:Name="togglePropInspector" ... />
...
</ToolBar>
...
<Border Visibility={Binding ElementName=togglePropInspector, Path=IsChecked, Converter={StaticResource BoolToVisibilityConverter}}">
...
</Border>
</UserControl>
I found this kinda neat as everything is handled inside the view and didn't require adding code to the view model (or code behind). However, the problem is that checking the toggle button on one tab now checks it in all instances of the view, not just the current tab. This basically applies to all elements whose state is not bound to the view model in any way. Is there a way around this without having to add code to the view model?
For completeness' sake here's the relevant part of how I'm loading the views:
<TabControl ItemsSource="{Binding Documents}">
<TabControl.Resources>
<DataTemplate DataType="{x:Type viewModels:DocumentViewModel}">
<local:DocumentView />
</DataTemplate>
</TabControl.Resources>
</TabControl>
TabControl has a single content host that is used for all TabItem instances. When the data models assigned to the TabItem.Content property are of the same data type, then the TabControl will reuse the same DataTemplate, which means same element instances, only updated with the changed data from data bindings.
To change the state of the reused controls, you must either access the control explicitly, or force the TabControl to reapply the ContentTemplate by temporarily changing the data type of the Content:
<TabControl SelectionChanged="TabControl_SelectionChanged" />
private void TabControl_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var tabControl = sender as TabControl;
var tabItemContainer = tabControl.ItemContainerGenerator.ContainerFromItem(tabControl.SelectedItem) as TabItem;
object currentContent = tabItemContainer.Content;
tabItemContainer.Content = null;
// Defer and leave the context to allow the TabControl to handle the new data type (null).
// The content switch shouldn't be noticable in the GUI.
Dispatcher.InvokeAsync(() => tabItemContainer.Content = currentContent);
}
You can also use a dedicated data type for each tab. This way the TabControl is automatically forced to switch the DataTemplate.
The cleanest solution would be to bind the ToggleButton to the data model.

Showing UserControl once at the time with DataTemplateSelector

I have a couple specific user controls to Show some Content, e.g. simple like Image, WebControl but also two complex specific custom controls drawing on a canvas.
Now I thought using the DataTemplateSelector to handle the different UserControls. I actully used this http://tech.pro/tutorial/807/wpf-tutorial-how-to-use-a-datatemplateselector as a reference.
I changed the code so the form loads the UserControls dynamically (according to the file extension) in the following collection:
ObservableCollection<string> _pathCollection = new ObservableCollection<string>();
The only difference to the reference is now I want to navigate back and forward to the next control by showing one control only at the time. Which control should I use instead of ListView?
<Grid>
<ListView ScrollViewer.CanContentScroll="False"
ItemsSource="{Binding ElementName=This, Path=PathCollection}"
ItemTemplateSelector="{StaticResource imgStringTemplateSelector}">
</ListView>
</Grid>
How do I need to bind it to the template (equal to ItemTemplateSelector above)? WPF is still very new to me and I am learning.
Use a ContentControl. Bind your current item to the Content-property and the DataTemplateSelector to the ContentTemplateSelector-property.
<ContentControl Content="{Binding Path=CurrentItem, Mode=OneWay}", ContentTemplateSelector="{StaticResource imgStringTemplateSelector}" />
Your CurrentItem should be a DependencyProperty or a INotifyPropertyChanged-property of your DataContext. When you change your CurrentItem, the ContentControl will update the template automatically with help of your TemplateSelector.

How to prevent NewItemPlaceholder row in a ComboBox bound to the same DataTable as a DataGrid in WPF

I'v a wizard style application that uses pages and the user can navigate between them either by Next and Previous buttons or by using a navigation bar to directely access certain pages.
On one page (i'll call it "grid page")i have a DataGrid bound to a DataTable. There is some initial data in the DataTable, but using the DataGrid the user can add, edit and delete rows as he wishes.
On the next page (i'll call i "combo box page") have a ComboBox which is bound to the same DataTable as the DataGrid on the grid page.
Both pages use the same object as data context.
If i jump to the combo box page directly everything works fine, the combo box has an entry for each row in the DataTable like it's supposed to be. Now i navigate to the grid page and don't touch anything there and then go to the combo box page again. Now the ComboBox displays a new item, a NewItemPlaceholder. Obviously this happens because the DataGrid has UserCanAddRows set to true and thus displays a placeholder row for a new item. But this should only concern the DataGrid and not the bound DataTable, so in my eyes this is a bug, or at least an absolutely horrible design.
Of course i don't want the NewItemPlaceholder in my ComboBox (and selecting it causes a lot of problems). So how can i prevent it from being displayed in the ComboBox?
Update: In the meantime i found out the placeholder item isn't in the DataTable as a row, which makes it even stranger, unless there is a flag in a DataTable that says "there is a NewItemPlaceholder in this table" but isn't a row itself. Additionally when i register to the Initialized event of the ComboBox i have the 2 items i'm looking for, when i register to the Loaded event i have the NewItemPlaceholder as well, so it must be added somewhere between those 2 events.
Use a CollectionViewSource. The problem occurs when more than one control shares the same view of a collection such as when a collection is bound in one case to a DataGrid and in another to the ComboBox. If the DataGrid allows the user to add items (i.e., the DataGrid's CanUserAddRows is true), the NewItemPlaceholder may have been added to the view. Try something like
<UserControl >
<UserControl.Resources >
<CollectionViewSource Source="{Binding MyCollection}"
x:Key="SourceWithoutNewItemPlaceholder" />
</UserControl.Resources>
<Grid Name="LayoutGrid" >
<ComboBox ItemsSource={Binding Source={StaticResource SourceWithoutNewItemPlaceholder}} >
</Grid>
</UserControl>
After several failed attempts i found 2 solutions:
1) In the Loaded event of the page i make a binding in the code behind, not to the DataTable itself, but to a DataTable.ToList. If i do it that way the NewItemPlaceholder won't show up.
2) In the view model i make a new property which returns the DataTable.ToList and bind to it. This way i can do the binding in XAML.
Just for reference, methods that don't work: Filter property on the ComboBox throws a NotSupportedException, seems the DataTable doesn't support this. Deleting the NewItemPlaceholder in the Loaded event of the ComboBox doesn't work either because you can't delete items when an ItemsSource is set. Removing the NewPlaceHolderItem when leaving the page with the data grid doesn't work either, because i can't find where it is (it's not in the DataTable or the View of the DataTable)
Use a CollectionViewSource to bind one control(Combobox) and collection property defined in ViewModel to bind another control (Datagrid).
Property in ViewModel :
private List<Rate> _rate = new List<Rate>();
public List<Rate> RatePlans
{
get { return _rate; }
set
{
_rate = value;
OnPropertyChanged("RatePlans");
}
}
Then in XAML :
<UserControl >
<UserControl.Resources >
<CollectionViewSource Source="{Binding RatePlans}" x:Key="RatesSource" />
</UserControl.Resources>
<Grid Name="LayoutGrid">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<ComboBox Grid.Row="0" DisplayMemberPath="RatePlanName" ItemsSource="{Binding Source={StaticResource RatesSource}}" HorizontalAlignment="Stretch"/>
<DataGrid Grid.Row="1" HorizontalAlignment="Stretch" CanUserAddRows="True" AutoGenerateColumns="False" ItemsSource="{Binding RatePlans}">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding RatePlanName}" Header="Rate Plan" />
<DataGridTextColumn Binding="{Binding RoomType}" Header="Room Type" />
<DataGridTextColumn Binding="{Binding Price}" Header="Price"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</UserControl>

Set WPF TabControl to show the same Content for every tab

I'd like to use a TabControl where each tab shows the same view just with different parameters. Therefore I do not want to create a new content control for each tab but reuse the same control for all tabs (binding some properties of it to the SelectedItem property of the TabControl)
I tried to my contained control as resource and set the Content property of the tab items to it, but this resulted in an exception, because the same element cannot appear as content in to different parents.
Any ideas?
<TabControl>
<TabControl.ContentTemplate>
<DataTemplate>
your view
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
I found a solution in this question
TabControl's TabItems sharing same content... Do Not Want
(even if the poster wanted specifically the opposite behaviour :) ...)

WPF DataTemplate - Event fired when a new item is added to Collection?

When a new item is added to the Flights collection a new TabItem is added to the TabControl. When a new tab is added, I need to call a method on the Chart control. The problem is I can't figure out the right event to handle.
My XAML looks something like the following:
<TabControl Name="chartControl" ItemsSource="{Binding Flights}">
<TabControl.ItemContainerStyle>
<Style TargetType="TabItem">
<Setter Property="Header" Value="{Binding Name}" />
</Style>
</TabControl.ItemContainerStyle>
<TabControl.ContentTemplate>
<DataTemplate>
<WindowsFormHost Name="winHost">
<legacy:Chart></legacy:Chart>
</WindowsFormHost>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
I tried handling the Loaded on the TabControl,
but duh that's only fired once.
I attempted a DataTemplate
Trigger on the RoutedEvent
FrameWorkElement.Loaded but I'm pretty sure that's not meant for my situation
I tried an EventSetter but that
didn't quite work the way I want
either
I attempted a few other things, but I don't quite remember them all.
Any suggestions would be greatly appreciated!
If I'm reading your XAML correctly, you are creating a single Chart control for the TabControl and changing its data when the TabItem changes? If so, you should be able to use the SelectionChanged event.
You might be better off putting your Chart control in the ItemTemplate so it automatically loads the selected Flights data when the user switches tabs or adds a new one.
Your Flights collection should be of type ObservableCollection<>. The ItemsSource binding in xaml will subscribe to its CollectionChanged event and add/remove tabs. As for calling the method on the Chart, does the WindowsFormHost have a Loaded event? Because a new one will be created for each tab that's created.

Resources