I have a TreeView on my page. It's bound to a collection of clients containing contracts, like:
public class Client
{
public int ClientID { get; set; }
public string Name { get; set; }
public List<Contract> Contracts { get; set; }
}
public class Contract
{
public int ContractID { get; set; }
public int ClientID { get; set; }
public string Name { get; set; }
}
The XAML for my TreeView is as follows:
<sdk:TreeView x:Name="tvClientContract" ItemsSource="{Binding ClientContracts}">
<sdk:TreeView.ItemTemplate>
<sdk:HierarchicalDataTemplate ItemsSource="{Binding Path=Contracts}">
<TextBlock Text="{Binding Path=Name}" />
</sdk:HierarchicalDataTemplate>
</sdk:TreeView.ItemTemplate>
</sdk:TreeView>
Where ClientContracts is a List<Clients>. The binding works fine and I have a hierarchical grid.
The issue that I'm having is when opening the form with the TreeView on it I want to select the current Client, I currently use the following code:
TreeViewItem client = (TreeViewItem)tvClientContract.ItemContainerGenerator.ContainerFromItem(aClient);
or
TreeViewItem client = (TreeViewItem)tvClientContract.ItemContainerGenerator.ContainerFromIndex(tvClientContract.Items.IndexOf(aClient));
client.IsSelected = true;
but this returns inconsistent results, for example I open the form when client 'ABC' is selected and client will be null. I open it again when client 'ABC' is selected and it returns the correct TreeViewItem. Has anyone came across this before or know anything I can look at to help solve the issue?
I run the above code within the Loaded event of the TreeView.
I figured out what's happening here, the clue is in the MSDN documentation for the return value of ItemContainerGenerator.ContainerFromItem():
A UIElement that corresponds to the
given item. Returns null if the item
does not belong to the item
collection, or if a UIElement has not
been generated for it.
It looks like when null is returned, a UIElement hasn't been created for the item yet.
I got round this by using
tvClientContract.UpdateLayout();
to update the layout and ensure a UIElement exists before calling
ItemContainerGenerator.ContainerFromItem()
I think that there could be some condition where "UpdateLoayout will not work":
if the TreeView is in recycling mode and the item is not in the visible portion and/or also in a "add" operation where the TreeViewItem is created on another thread.
The solution is to use similar solution as I describe in:
WPF: Select TreeViewItem broken past the root level
Related
I have a databinding problem in Windows Forms.
Here's the part of the EF model that is relevant for the story:
namespace Model
{
class Person
{
[Key]
public int Id { get; set; }
public String Name { get; set; }
public virtual ICollection<Receipt> Receipts { get; set; }
}
class Receipt
{
[Key]
public int Id { get; set; }
public DateTime Timestamp { get; set; }
public double Value { get; set; }
public int PersonId { get; set; }
public virtual Person Person { get; set; }
}
}
There is a BindingSource called peopleBindingSource that is set to class Model.Person. In code behind, I do this:
db.People.Include(p => p.Receipts).Load();
peopleBindingSource.DataSource = db.People.Local.ToBindingList();
Textbox has its DataBindings set to peopleBindingSource as DataSource and Name as the property.
When I run the application, the binding navigator works correctly, showing different people as I click previous and next buttons. The textbox with the person's name is also populated correctly.
Now, here comes the problem:
On the same form, there is a DataGridView. I want to show a row for each of the Receipt of currently selected Person.
I've tried to bind dataGridView's DataSource to Receipts navigation property of peopleBindingSource.
I have expected that the gridview will update its DataSource to the appropriate collection of receipts just as the textbox updates its Text property on navigation change. The setup is basicly the same.
The thing is, no error is thrown, and the grid remains empty (it doesn't even create the columns header).
Why doesn't this binding work and can this be done from within the designer? How to correctly set up binding (without programatically handling change event on peopleBindingSource and setting grid's DataSource manually from peopleBindingSource.Current)?
I have found the solution to the problem.
The thing is that DataGridView can't bind to ICollection<Receipt> so the solution was to change the navigation property to BindingList<Receipt>. Then the designer showed the property correctly.
I've got a ComboBox (inside a ListView) that used to be tied to a collection of strings. However, I'm switching to having it use a custom class instead.
Now the ComboBox is bound to an ObservableCollection of type Area. For display, I'm showing the Name property with DisplayMemberPath. This works great, but the box is no longer loading with the current value selected.
<ListView x:Name="TestListView" ItemsSource="{Binding TestViewList, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<ListView.View>
<GridView>
<GridViewColumn Header="Area">
<GridViewColumn.CellTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding DataContext.AreaList, ElementName=BVTWindow}" DisplayMemberPath="Name" SelectedItem="{Binding Path=Area, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" />
public ObservableCollection<ViewTest> TestViewList { get; private set; }
public ObservableCollection<Area> AreaList { get; private set; }
public class ViewTest : BindableBase
{
public string Description { get; set; }
public Area Area { get; set; }
}
public partial class Area
{
public long ID { get; set; }
public string Name { get; set; }
}
"Area" is the same class as the Collection, and is a property of the class the ListView's ItemsSource is bound to. Am I doing this the right way? Any help is much appreciated.
UPDATE:
I had a theory that perhaps using the "Name" property (which is a string) for display in the box made the SelectedItem attribute look for a string rather than something of type Area. I changed the class for TestViewList to use a string to keep track of Area, but that didn't change the program's behavior at all.
UPDATE 2:
Above, I've added the pertinent lines from the view model related to the ComboBox and ListView in question.
Update 3:
I've changed my Area property from inline to expanded, including the SetProperty that handles raising. It now works for new items added to the ItemSource at run-time, but is still a blank selection for on items loaded at program start.
public class ViewTest : BindableBase
{
public string Description { get; set; }
public Area Area
{
get
{
return this.area;
}
set
{
this.SetProperty(ref this.area, value);
}
}
}
Update 4:
It turns out Paul Gibson's answer was correct. The reason my existing entries weren't loading properly had to do with a logic error in the way I was loading items from the database, and not a problem with the xaml bindings. Everything works now (with respect to those ComboBoxes, at least).
Based on what you are saying I think my comment is the answer. This is the case for a single combobox. In your case you need one for every line of the grid, so you may have to programatically add the binding to members of a list by index.
In your view model if you also have (single case):
private Area _curSelArea;
public Area curSelArea
{
get { return _curSelArea; }
set
{
_curSelArea = value;
RaisePropertyChanged("curSelArea");
}
}
Then you can bind to the property with:
SelectedItem="{Binding DataContext.curSelArea . . . }"
The view model can set the initial value of curSelArea if it is known initially.
EDIT: After actually having to do this I found that someone extended the DataGridComboBoxColumn to facilitate better binding. Check out this link if you are trying to do this: http://joemorrison.org/blog/2009/02/17/excedrin-headache-35401281-using-combo-boxes-with-the-wpf-datagrid/
I'm using my own derived ObservableCollection that implements ICollectionViewFactory to allow me to create my own IEditableCollectionView on top of it. The (main) purpose of the view is to allow filtering out objects flagged as "deleted", so that these records aren't shown to the user but still remains within the collection, as long as they're not marked as "accepted" or rolled back.
Am I on the right track here? Or this isn't the purpose of IEditableCollectionView?
UPDATE: The collection must support adding, removing and editing records.
SECOND UPDATE: The records flagged as "deleted" must still be within the source collection, as a delete operation can be rolled back.
I think what you are after can be achieved much more easily
Say you have a Model
public class Item
{
public bool IsDeleted { get; set; }
public string Name { get; set; }
}
And your ViewModel contains a collection
public ObservableCollection<Item> MyItems { get; set; }
You can add ICollectionView property that will filter your collection by undeleted items. Here is an example:
public ICollectionView UndeletedItems { get; set; }
Filtering logic:
// Collection which will take your ObservableCollection
var itemSourceList = new CollectionViewSource { Source = MyItems };
// ICollectionView the View/UI part
UndeletedItems = itemSourceList.View;
//add the Filter
UndeletedItems.Filter = new Predicate<object>(item => !((Item)item).IsDeleted);
Then bind your View to UndeletedItems instead
<DataGrid ItemsSource="{Binding UndeletedItems}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Name}"/>
</DataGrid.Columns>
</DataGrid>
This will hide deleted items while still supporting CRUD operations.
Hope this helps
I have a simple datastructure following:
In the model I have
public class Receipt
{
public int Id { get; set; }
public double Price { get; set; }
public string Store { get; set; }
public DateTime Date { get; set; }
}
I've made two of these objects and I am trying to bind them to a datagrid. I've filled in the properties of the two receipts and added them to the dataGridRows but they don't show up in my DataGrid.
public MainWindow()
{
InitializeComponent();
makeReceipts()
}
public ObservableCollection<Receipt> dataGridRows = new ObservableCollection<Receipt>();
public Receipt receipt1 = new Receipt();
public Receipt receipt2 = new Receipt();
public void makeReceipts()
{
receipt1.Id = 1;
receipt1.Price = 10;
receipt1.Store = "Brugsen";
receipt1.Date = DateTime.Today;
receipt2.Id = 2;
receipt2.Price = 15;
receipt2.Store = "Netto";
receipt2.Date = DateTime.Today;
dataGridRows.Add(receipt1);
dataGridRows.Add(receipt2);
}
And in the xaml of the MainWindow where I want my datagrid to display the receipts I have:
<DataGrid Name="ReceiptGrid" CanUserResizeColumns="True" IsReadOnly="True" AutoGenerateColumns="True" ItemsSource="{Binding Source=dataGridRows}" />
What am I doing wrong?
first you can just bind to public properties.
so if you want to use binding you have at least do:
public ObservableCollection<Receipt> dataGridRows {get;set;}
second you have to do two steps:
set the right datacontext
set the right binding expression(Path)
assume that the datacontext for yyour grid is an object with the property dataGridRows, your binding should look like this
<DataGrid ItemsSource="{Binding Path=dataGridRows}" .../>
Think your problem is you have to write
ItemsSource="{Binding Path=dataGridRows}"
and not
ItemsSource="{Binding Source=dataGridRows}"
source is to specify another control in xaml file
First of all you can bind only public properites so you need to change definition of dataGridRows to something like this:
public ObservableCollection<Receipt> dataGridRows { get; set; }
then you don't bind it as a Source but as a Path, however since your dataGridRows is defined in MainWindow you need to specify Source as your MainWindow otherwise it will look in default DataContext which is not set in your case
<DataGrid ... ItemsSource="{Binding RelativeSource={RelativeSource AncestorType={x:Type Window}}, Path=dataGridRows}" />
this tells Binding to find Window and look for a dataGridRows property there.EDIT:Normally you don't put data into view. I suggest you read more about MVVM design pattern but basically the idea is that you have your ViewModel where you put in you whole application logic, unaware of interface, and then on top you have your view to interact with user but in ViewModel you don't operate on controls.What you should do is create your view-model class with dataGridRows property and assign it through DataContext of Window for example. Each FrameworkElement has it and when you don't specify Binging source (Source, RelativeSource, ElementName) it will try to resolve Binding.Path in current DataContext. If current control does not have it specified then if will go to parent in visual tree and so on.
Initially I posted this to the PRISM4 forum but got a suggestion that I should
try this forum as well:) I'm using WPF4 BTW...
I'm running PRISM4 and I've been struggling to get my data binding to work.
I'm following the MVVM pattern and have a view model which initially loads
data from a RDBMS and wraps it in an ICollectionView.This works perfectly, the data is displayed in the bound DatGrid, but I'm struggling in my eforts when trying to persist
changes made to the data which is presented in a DataGrid declared below.
The view model publishes the ICollectionView through a read/write property,
"Results", which, as you can see has a binding mode of "TwoWay". I thought
this would be enough to persist the changes made to the state of the
checkboxes but no:( I've experimented with a number of ways to accomplish
this but the state of the checkbox is not propagated back to the view model.
I've intercepted the call to the "PlotClicked" method which is an ICommand
object but the argument being passed has an unchanged "Plot" attribute!
This is especially obvious when I click one of the column headers and the
view is sorted - the checked rows are unchecked which is the default state of the checkboxes when retrieved from the db.
What am I doing wrong here?
Many thanks in advance - I'm really stuck here:(
/Peter
<DataGrid Grid.Row="0" Name="gridResults" ItemsSource="{Binding Results,Mode=TwoWay}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Plot">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding Path=Plot, Mode=TwoWay}"
HorizontalAlignment="Center"
Command="{Binding Path=DataContext.PlotClicked,Mode=OneWay, RelativeSource={RelativeSource FindAncestor,AncestorType={x:Type DataGrid}}}"
CommandParameter="{Binding SelectedItem, RelativeSource={RelativeSource FindAncestor,AncestorType={x:Type DataGrid}}}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
...
I tried out the suggestions pointed out to me. This is what I've done:
In the view-model I changed the Result property from ICollectionView to OC
public ObservableCollection Results { get; set; }
Added the following template resources for the UserControl making up the View
Added the following code to the DataGrid in the "Columns" section
<DataGridTemplateColumn
Header="cbTest"
x:Name="cbTest"
CellTemplate="{StaticResource IsSelectedColumnTemplate}"
CellEditingTemplate="{StaticResource IsSelectedColumnTemplateEditing}"
CanUserSort="True"
Width="Auto"
/>
After having made those changes I experimented with various settings for the UpdateSourceTrigger in the IsChecked="{Binding mentioned... in (2) above with no effect. The changes I make to the checkboxes are not transferred back to the view-model's ObservableCollection.
Again, many thanks for trying to help me out here!
* UPDATE *
Now I've experienced something REALLY SPOOOOKY:( This is what I've done:
public class ResultViewResult : IReslutViewResult
{
public bool Plot { get; set; }
public Guid ResultId { get; set; }
public DateTime Generated { get; set; }
public int Duration { get; set; }
...
This didn't work in a sense that the 'Plot property' could NEVER be set to true by clicking the checkbox column in the DataGrid! Now I did the following:
public class ResultViewResult : IReslutViewResult
{
private bool _plot;
public bool Plot
{
get
{
return _plot;
}
set
{
_plot = value;
}
}
public Guid ResultId { get; set; }
public DateTime Generated { get; set; }
public Guid ResultId { get; set; }
public DateTime Generated { get; set; }
public int Duration { get; set; }
...
The result you may ask? It works and the 'Plot' is correctly set! Now, I thought, this is weird!!! So what I did was the following (simply commenting out the private var and get/set code):
public class ResultViewResult : IReslutViewResult
{
public bool Plot { get; set; }
//private bool _plot = false;
//public bool Plot
//{
// get
// {
// return _plot;
// }
// set
// {
// _plot = value;
// }
//}
public Guid ResultId { get; set; }
public DateTime Generated { get; set; }
public int Duration { get; set; }
...
Ok, what about the result? IT WORKS!!!??? I'm stunned... I mean what's the difference between the first and the last???? I feel VERY awkward about this - I mean I want to know WHAT'S going on behind the scene here... :(
Not sure about that, but I'd suggest you to try with an ObservableCollection used as ItemsSource . I had a lot of problems like that before, all of them were solved using this kind of collection (which is btw faster and less consuming than a classic collection for refreshing purposes).
Also, try to add in the IsChecked binding the following: UpdateSourceTrigger=PropertyChanged, this could do the trick, I think the only problem here is that the source isn't updated at the right time!
Not sure if this is the issue in your case, but the DataGrid uses a variation of databinding that will not commit the changes to the source until you move off the row. This is called a BindingGroup. Maybe you're not seeing the values committed because you haven't moved off the row yet?
http://blogs.msdn.com/b/vinsibal/archive/2008/08/11/wpf-3-5-sp1-feature-bindinggroups-with-item-level-validation.aspx
One other possibility is that the binding path is somehow not correct? Have you checked the output window in VS to see if it's reporting any binding path failures?