wpf datagrid autoscroll - wpf

I would like to set up a datagrid so that whenever an item is added to its itemssource the datagrid scrolls down to show the last item.
The datagrid is inside a datatemplate, so i cannot set the X:name property and access it directly from the codebehind.
What I have in mind is to use a datagrid event that fires when a row is added and has the grid scroll itself.
Here's some psuedo code that outlines how I have things set up:
UI.XAML exerpt
<TabControl ItemsSource="{Binding Parents}" x:Name="ProductsTab">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Key}"/>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<DataGrid Margin="5" ItemsSource="{Binding Value.Children}">
<DataGrid.Columns>
<Column Column definitions removed for your sanity/>
</DataGrid.Columns>
</DataGrid>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
UI.XAML.CS exerpt
public class UI
{
//Thanks to Dr. WPF for the ObservableDictionary class
public ObservableDictionary<string, Parent> Parents {get; set;}
}
Parent.CS
public class parent
{
public ObservableCollection<Child> Children {get; set;}
}
The datagrids are not editable.
In case you're wondering, I have read the post "How to autoscroll on WPF datagrid" the code in that post would work for me if I could find an event that fires whenever an item is added to the datagrid itemssource.
Any Ideas?

Combine the autoscrolling idea with the idea from this question or this MSDN thread: instead of listening to your grid's event to detect row additions, listen to the events from the ItemsSource.
Edit: Since you don't like that suggestion, you could try hooking LoadingRow, but I strongly suspect this will require EnableRowVirtualization = false in order to work (I haven't tried it). If your collection gets large, turning off row virutalization opens up the possibility of a serious performance hit.

you can access the DataGrid, even if it is in a DataTemplate, by doing a 'search' in the visual tree : VisualTreeHelper.GetChildCount // VisualTreeHelper.GetChild , then test again the type until you find your grid. And you might seek with same kind of methods the ScrollBar, and then you can hook an event handler and use a code-behind logic.

Related

Dynamic databinding ListBox + WPF + FlowDocument

I have a listBox, which I am trying to bind to an IList collection using ItemsSource. My problem scenario comes when each of my person object has a FlowDocument which I am trying to display in a richTextBox within the listBoxItem.
Imagine the performance degradation, when there are 1000 person objects,
Is there a way, I get to dynamically load the flowDocument / RichTextbox so that there is no performance impact.
Is there a way, I get to know which Items of the listbox are visible at any moment in time so that, I can dynamically bind the richtextbox with the flow document and when the scroll happens, I can clear the previous binding and apply binding to only those items that are visible.
<ListBox ItemsSource="{Binding PersonsCollection">
<ListBox.ItemTemplate>
<DataTemplate>
<RichTextBox Document="{Binding PersonHistory}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
thanks
public class Person
{
public FlowDocument PersonHistory{get;set}
}
You can separate the UI in two controls to improve the performance. Consider adding a unique attribute in person class like primary key in a database table.
public class Person
{
public long ID{get;set;}
public FlowDocument PersonHistory{get;set}
}
Now you can have one ListBox
<ListBox Name="PersonsListBox" ItemsSource="{Binding PersonsCollection"} DisplayMemberPath="ID" SelectionChanged="personsList_SelectionChanged">
</ListBox>
With which you bind the PersonsCollection and set DisplayMemberPath="ID" to show only ids in ListBox.
And you have a RichTextBox separately in your xaml.
<RichTextBox Name="personHistoryTextBox"/>
If you see I have added an event with ListBox as well. The SelectionChanged Event.
In your event you can do something like this.
private void personsList_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
if(PersonsListBox.SelectedItem != null){
personHistoryTextBox.Document = (PersonsListBox.SelectedItem as Person).PersonHistory;
}
}

Binding ObservableCollection of strings to templated ListBox

I have this collection
ObservableCollection<string> Urls { get; set; }
inside my data context class. I have a binding to it in my list box:
<ListBox ItemsSource="{Binding Urls}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBox Text="{Binding .}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
The data is diplayed in the list box, the two corresponding-not-shown-here buttons with commands Add and Delete work as well, however, the changing the TextBox does not affect the contents of the collection.
I have tried Mode=TwoWay in binding, but I figured that it is turned on already. I have tried some other options like Validate=OnPropertyChange, however, there is still nothing get updated.
How to make the TextBox inside that templated items in ListBox actually update the Urls property of a datacontext class?
You cannot modify strings; use a wrapper class with one string property, then bind the TextBox to said property. That way the strings in the property can be replaced with the edited ones.

How can I perform a custom action on the data of a DataGridRow when it is selected?

I've been trying to figure out how to get this custom behaviour into a datagrid with out having much look when searching online for solutions.
Given the following datagrid (some xaml removed for brevity):
<DataGrid ItemsSource="{Binding Items}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTemplateColumn Width="auto">
<DataGridTemplateColumn.HeaderTemplate>
<DataTemplate>
<CheckBox />
</DataTemplate>
</DataGridTemplateColumn.HeaderTemplate>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding Selected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
I have the checkbox successfully bound to the databound object for each row. (Note: I'm using a DataGridTemplateColumn rather than DataGridCheckBoxColumn so that you do not need to double-click to change the value).
What I would like to achieve is to have the ability to tick the checkbox / update the Selected property of the databound object when the user selects a row. Effectively making the entire row click set the checked property of the checkbox. Ideally, I'd like to do this without a code behind file if possible as I'm trying to keep my code behinds as clean as possible.
Another feature which I would like, if possible, would be that clicking on a row would toggle it's selected property so that if you click on another one, the previous one stays selected as well as the new one.
Any help is much appreciated.
For clarity. I understood
Another feature which I would like, if possible, would be that
clicking on a row would toggle it's selected property so that if you
click on another one, the previous one stays selected as well as the
new one.
in the way, that you want the CheckBox of the an item, respectively the Selected property on the items ViewModel, to stay selected, when the next DataGridRow is selected, but not the DataGridRow itself? Is that correct?
My suggestion is to extend the behavior of your DataGrid using *WPF behavior*s (This is a good introduction. This way you can keep your codebehind clear, but don't have to twist XAML to make it do what you want.
This is basically the idea of behaviors: Writing testable code, which is not coupled to your concrete view, but nonetheless allowing you to write complicated stuff in 'real' code and not in XAML. In my opinion your case is a typical task for behaviors.
Your behavior could look about as simple as this.
public class CustomSelectionBehavior : Behavior<DataGrid>
{
protected override void OnAttached()
{
// Set mode to single to be able to handle the cklicked item alone
AssociatedObject.SelectionMode = DataGridSelectionMode.Single;
AssociatedObject.SelectionChanged += AssociatedObject_SelectionChanged;
}
protected override void OnDetaching()
{
AssociatedObject.SelectionChanged -= AssociatedObject_SelectionChanged;
}
private void AssociatedObject_SelectionChanged(object sender, SelectionChangedEventArgs args)
{
// Get DataContext of selected row
var item = args.AddedItems.OfType<ItemViewModel>();
// Toggle Selected property
item.Selected = !item.Selected;
}
}
Attaching the behavior to your specific DataGrid, is done in XAML:
<DataGrid ...>
<i:Interaction.Behaviors>
<b:CustomSelectionBehavior />
</i:Interaction.Behaviors>
...
</DataGrid>
You need to reference
System.Windows.Interactivity.dll
which contains the Behavior<T> baseclass as well.

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>

MVVM ListBox controlling a Content Control

I've been going round in circles with this for a couple of days, and I'm hoping a WPF guru can see where I'm going wrong.
I'm setting CurrentViewModel in code. The Selected item of my ListBox and the Content of my ContentControl bind correctly. But when changing the selected item in the Listbox via the UI the CurrentViewModel is being set but the Content Control is not being updated.
I'm using a data template to map my Views and View Models.
<DataTemplate DataType="{x:Type ViewModel:MyViewModel}">
<View:MyView />
</DataTemplate>
I have a ListBox which is bound to an observable collection of ViewModels. The Selected Item is bound to the current view model.
<ListBox ItemsSource="{Binding MyViewModelCollection}" DisplayMemberPath="DisplayName" SelectedItem="{Binding CurrentViewModel, Mode=TwoWay}"/>
I also have a content control that is also bound to the CurrentView Model
<ContentControl Content="{Binding CurrentViewModel, Mode=TwoWay}"/>
This is the property that they are both bound to
public MyViewModel CurrentViewModel
{
get
{
return _currentViewModel;
}
set
{
if (_currentViewModel== value) return;
_currentViewModel= value;
OnPropertyChanged("CurrentViewModel");
}
}
I've edited the names for clarity and removed formatting information.
Any help greatly appreciated.
Cheers,
Daniel
EDIT: Came across the link How can I debug WPF bindings?. I set a break point on the Content binding and it does indeed only get called once when the binding is first set.
You should not be setting TwoWay as the mode on your ContentControl:
<ContentControl Content="{Binding CurrentViewModel, Mode=OneWay}"/>
This is because you intend your ContentControl to read the value, but never write it.
As an aside, you can also bind the ContentControl to the currently selected item in the collection, rather than to that property by doing this:
<ListBox ItemsSource="{Binding MyViewModelCollection}"
DisplayMemberPath="DisplayName"
IsSynchronizedWithCurrentItem="True"/>
<ContentControl Content="{Binding MyViewModelCollection/}"/>
The "slash" (/) at the end of the collection indicates the current item selected in the collection and setting that current item property is as simple as setting the IsSynchronizedWithCurrentItem equal to true.
A lot of times I find with this combination, I really don't need the extra property on my view model.
Anyway, I hope this helps.

Resources