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
Related
I am building an MVVM app with AvaloniaUI/ReactiveUI and EF Core to be able to edit a few tables with data.
I guess the fact that I use AvaloniaUI does not play too much of a role though.
If I had used WPF the problem would probably be the same (seems more to be a ReactiveUI issue).
As an example assume two tables, Classes and Instructors, where Classes holds a foreign key to the Instructors table.
Classes and Instructors have the following models:
class Class
{
//Primary key
public int ID { get; set; }
public string Name { get; set; }
//Foreign key to Instructors table
public int InstructorID { get; set; }
//Navigation property
public Instructor Instructor { get; set; }
}
class Instructor
{
//Primary key
public int ID { get; set; }
public string Name { get; set; }
}
The main view model contains collections both of these as
public List<Class> Classes { get; set; }
public List<Instructor> Instructors { get; set; }
I want the classes to be displayed in a DataGrid. For that I used
programmatic (type-safe) ReactiveUI bindings like so
<DataGrid Name="DGClasses">
<DataGrid.Columns>
<DataGridTextColumn Header="Name"
Binding="{Binding Name}" />
<DataGridTextColumn Header="Instructor"
Binding="{Binding Instructor.Name}"/>
</DataGrid.Columns>
</DataGrid>
...
this.OneWayBind(ViewModel, x => x.Classes, x => x.DGClasses.Items);
this.Bind(ViewModel, x => x.SelectedClass, x => x.DGClasses.SelectedItem);
...
So far this works well, the Instructor column even displays the name
of the instructor although the Classes table contains only the foreign key
into the Instructors table (this works by means of the navigation property
in the Class class).
For editing the Instructor of a class the column should use a ComboBox
as editor so I can select from all existing Instructors. I started with
<DataGridTemplateColumn Header="Instructor">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox Items="{Binding ??? }">
</ComboBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
But this is where I'm stuck. A direct binding expression does not work
because the Instructors collection is not a subproperty of Class and
I know of no way to programmatically bind the Items property (ItemsSource
in WPF) in a DataTemplate in a DataGrid column. Since I use the programmatic
ReactiveUI binding with all (top-level) elements I also don't have set
the DataContext property of the Window that contains the DataGrid.
Any idea how to accomplish this?
You can bind your combobox item collection to some special attached property on the DataGrid (or just use Tag) and do something like {Binding $parent[DataGrid].Tag}.
I will try to simplify as much as possible so:
I have following EF6 entity:
public class ParentObject
{
ICollection<ChildObject> Children {get; set ;}
}
I have ViewModel where is
ObservableCollection<ParentObject> ParentCollection { get; set; }
ParentObject SelectedParent { get; set; }
ChildObject SelectedChild { get; set; }
Also I have two ListViews
<ListView Name="lvParents" Margin="5,5,0,5" Grid.Row="1" Grid.Column="0" ItemsSource="{Binding Path=ParentCollection}" SelectedItem="{Binding Path=SelectedParent}">
<ListView Name="lvChildren" Margin="5,5,0,5" Grid.Row="1" Grid.Column="0" ItemsSource="{Binding Path=SelectedParent.Children}" SelectedItem="{Binding Path=SelectedChild}">
All EF6 Entities have INotifyPropertyChanged implemented,
Also my ViewModel has INotifyPropertyChanged Implemented.
Everything is working well, if I'm changing selected item in lvParents. Children in lvChildren are changing.
Once I delete the children, I cannot refresh the Children:
(item is deleted from DB, but cannot refresh just lvChildren.
Here is method I tried to use:
_dataContext.ChildrenObjects.Remove(SelectedChild);
_dataContext.SaveChanges();
SelectedParent.Children.Remove(SelectedChild);
//here the SelectedParent does not contain the value I removed but no way how to refresh the ItemsSource of lvChildren.
NotifyPropertyChanged("SelectedChild");
NotifyPropertyChanged("SelectedParent.Children");
NotifyPropertyChanged("ParentCollection");
My question is, what is the correct way to bind depended (related) collections and refresh them after they are changed. Do I need overload the EF6 ICollection and create my own ObservableCollection ???
Is it enough?
My question is, what is the correct way to bind depended (related) collections and refresh them after they are changed?
The Children property of the ParentObject should return an ObservableCollection:
public class ParentObject
{
ObservableCollection<ChildObject> Children {get; set ;}
}
Then the ItemsSource will be automatically refreshed when you remove an item from the collection:
SelectedParent.Children.Remove(SelectedChild);
That would be the most "correct" way of implementing this according the MVVM design pattern.
Using and binding to auto-generated entity types "as-is" is rarely a good idea in WPF. You should bind to ObservableCollection<T> properties rather than ICollection<T> ones if you want to be able to dynamically modify your collections at runtime.
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/
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?
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