How should I implement a CollectionView with inherited entities? - c#

I'm new at WPF/C# and I have a question about binding Collections using Entity Framework.
I have a base Entity"Order" (Commande in french) and I used Inheritance per type to regroup them in fonctionnal topics : by instance : Order_Doc, Order_Check , ...
I put controls on tabitem with each item containing a panel of controls I'd like to bind with child entity fields.
So I'm searching an elegant solution to fill these tab items with the associated Child Entity.
As yet, I've managed to bind controls with data belonging to the base Class Order and to walk through the collection.
I've declared a CollectionViewSource based on Orders.
<Window.Resources>
<CollectionViewSource x:Key="ViewSourceCommandes" d:DesignSource="{d:DesignInstance src:Commande, CreateList=True}"/>
</Window.Resources>
When the window is loaded I Initialise the collection of Orders (commandes)
CommandesViewSource = ((CollectionViewSource)(this.FindResource("ViewSourceCommandes")));
ObjectQuery<Commande> CommandesQuery = this.GetCommandesQuery(contexte);
CommandesViewSource.Source = CommandesQuery.Execute(MergeOption.AppendOnly);
The function GetCommandesQuery is simple :
private ObjectQuery<Commande> GetCommandesQuery(ModelContainer CommandeEntities)
{
ObjectQuery<Commande> CommandesQuery = CommandeEntities.Commandes;
return CommandesQuery;
}
I can walk through the collection by this function :
private void buttonFind_Click(object sender, RoutedEventArgs e)
{
Int32 NumCommande = Convert.ToInt32(textBoxNumCommande.Text);
Commande commandeItem = contexte.Commandes.Where("it.NumCommande = #NumCommande", new ObjectParameter("NumCommande", NumCommande)).FirstOrDefault();
CommandesViewSource.View.MoveCurrentTo(commandeItem);
}
Some controls on my main Window are binded on "Commande" fields : I can get the right content for them.
But there's also in this main Window a TabControl in which I'd like to feed TabItems with
inherited classes as Commande_Doc, Commande_Check, ... and to bind controls on their fields.
What is the best way to implement this elegantly ? I'm a bit lost ...
Thanks by advance.

Related

How to get access to parent's DataContext

how can I get access to a parent's DataContext?
I've got an UserControl containing 3 buttons, which I want to use for several different UserControls - so the user has always the same actions available.
When clicked on button 'Add' I need to do something inside the current DataContext, which isn't much of a hassle since I can just do the following:
public void CtrlClicked(object sender, RoutedEventArgs e){
Button btn = sender as Button;
MyClass2 c2 = btn.DataContext as MyClass2;
c2.CallCustomMethod();
}
When the button 'Del' is clicked I want to delete the object MyClass2 out of a List<MyClass2> which is held in MyClass1.
In order to do that I need to have access to MyClass1.
My UI (pseudo code):
Window (DataContext = base)
Grid
UserControl uc1 (DataContext = base.MyClass1)
Grid
ListView
ListView.DataTemplate
UserControl uc2 (DataContext = base.MyClass1.MyClass2)
Grid
UserControl ucButtons
Grid
UserControl uc2
ListView.DataTempate
ListView.PanelTemplate
UniformGrid
ListView.PanelTemplate
ListView
Grid
UserControl uc1
Grid
Window
So how can I get access to the MyClass1-objext?
I found out that I can walk the tree using .Parent, but can only do that to a certain point:
Grid gScheduleControlBar = btn.Parent as Grid;
UserControl ucScheduleControlBar = gScheduleControlBar.Parent as UserControl;
Grid gDay = ucScheduleControlBar.Parent as Grid;
UserControl ucDay = gDay.Parent as UserControl;
//ucDay.Name confirms it's the userControl defined
Grid grid = ucDay.Parent as Grid;
// grid.Name="" and grid.Parent = null
so from here there is no further way upwards, which means I can't pass the UserControl 'border'.
Any ideas?
As fallback-option there is of course the way of storing a reference of MyClass1 in MyClass2.
EDIT => Final Result:
<Button x:Name="Del" DataContext="{Binding RelativeSource={RelativeSource Mode=FindAncestor AncestorType=UserControl AncestorLevel=3}}"
If you want to do this via Bindings, you can use RelativeSource={RelativeSource Mode=FindAncestor AncestorType=yourNamespace:YourType}, from code you can use the VisualTreeHelper to get the visual parent of any control.
If there are multiple parents of that type in your hierarchy can can additionally specify an AncestorLevel. In the example you included, it looks like AncestorType=UserControl and AncestorLevel=2 should work.

Implementing an options dialog

in my application i want to implement an options dialog like you have in VisualStudios if you go to Tools->Options in the menubar. How can i do this? My first idea was to use pages and navigation but maybe there's an easier approach?
It's probably not the easiest way but I wrote this snippet that match your goal and it's a good exercise.
In an empty Windows Forms project add a ListBox (listBox1) and a Panel (panel1). Then create 2 UserControls (UserControl1 and UserControl2), these will be the content that is shown when you click the list.
In your Form1 class we create a ListItem class that will contain your menu options as such:
public partial class Form1 : Form
{
public class ListItem
{
public string Text { get; set; }
public UserControl Value { get; set; }
public ListItem(string text, UserControl value)
{
Text = text;
Value = value;
}
};
...
}
After that you add items to the ListBox right after InitializeComponent() in Form1:
public Form1()
{
InitializeComponent();
listBox1.DisplayMember = "Text";
listBox1.ValueMember = "Value";
listBox1.Items.Add(new ListItem("Item1", new UserControl1()));
listBox1.Items.Add(new ListItem("Item2", new UserControl2()));
}
This will make it so when you use listBox1.SelectedItem it will return an object that you can cast to a ListItem and access the associated UserControl.
To make use of this behaviour, go to designmode and double-click the ListBox, this'll add code for the SelectedIndexChanged event. We use this event to display the UserControl in the Panel panel1. This will clear any old Panel content and add a selected UserControl:
private void listBox1_SelectedIndexChanged(object sender, EventArgs e)
{
panel1.Controls.Clear();
UserControl control = (listBox1.SelectedItem as ListItem).Value;
if(control != null)
{
panel1.Controls.Add(control);
control.Dock = DockStyle.Fill;
}
}
I suggest you try adding a button or something to differentiate the UserControls and play around. Have fun! :)
You should create a new Window and show that as opposed to create a page and navigate to it. Then you would call .show() on the new window for it to show.
Then you would change the look of the new window to however you want, the same as editing pages.
If you build your options into a full object model that matches the structure of the options window, then the best way is to use whatever navigation-aware UI binding that your MVVM toolkit uses. The options window would start off as a new root level window to which you would bind the root of your options data model.
So, in short think of the options dialog as a mini-application that uses the same structure as your main MVVM application, but with a different data model root.
If you plan to allow the user to cancel the changes to the options, then you would want your options data model to be clonable so that you can populate the options window with the clone and then swap out the real options with the new data if the user presses OK on the options window. If they select cancel you can just throw the cloned object away and destroy the window.

Multiple Views of Observable Collection with Datagrid

I have the same problem like this. But I´m using a DataGrid instead of a ListBox and it does not seem to work like this (it might also be because i never used visual basic and didnt translate the code correcly into c#).
I basicly want two DataGrids on the same data with different filters.
ICollectionView view_dataLinesUnfiltered;
ICollectionView view_dataLinesFiltered;
public MainWindow()
{
...
//view_dataLines = CollectionViewSource.GetDefaultView(dataLines); // <- Filter works on both
view_dataLinesUnfiltered = new CollectionView(dataLines); // <- Filter doesn´t work at all
view_dataLinesFiltered = new CollectionView(dataLines);
....
// Control Events
this.ShowAA.RaiseEvent(new RoutedEventArgs(System.Windows.Controls.Primitives.ToggleButton.UncheckedEvent));
}
private void ShowAA_Checked(object sender, RoutedEventArgs e)
{
view_dataLinesUnfiltered.Filter = null;
}
private void ShowAA_UnChecked(object sender, RoutedEventArgs e)
{
view_dataLinesUnfiltered.Filter = delegate(object o) { return FilterContent(o as ErrorDetection.stDataLine, "AA", ""); };
}
bool FilterContent(ErrorDetection.stDataLine line, string sFilterAA, string sFilter)
{
shortArrayToHexStringConverter converter = new shortArrayToHexStringConverter();
string comBuffer = converter.Convert(line.ComBufferP as object,typeof(string),0,System.Globalization.CultureInfo.CurrentCulture) as string;
return false;// !comBuffer.Contains("AA");
}
The FilterContent method is being called without problems, but the DataGrid shows the lines anyway. If I use GetDefaultView the Filter works on both Datagrids. Do I have to use some other view instead of CollectionView (ListCollectionView does also not work)?
i have made a small sample project to show the problem sample. It only consists of an constructor and an observable collection.
I got it to work somehow. I used CollectionViewSources now instead of ICollectionView.
<Window.Resources>
<CollectionViewSource x:Key="viewSource_dataLinesUnfiltered"/>
<CollectionViewSource x:Key="viewSource_dataLinesFiltered"/>
</Window.Resources>
...
<DataGrid Name="Filtered_Datagrid" ItemsSource="{Binding Source={StaticResource viewSource_dataLinesFiltered}}" >
...
</DataGrid>
...
<DataGrid Name="Unfiltered_Datagrid" ItemsSource="{Binding Source={StaticResource viewSource_dataLinesUnfiltered}}">
...
</DataGrid>
and the c Code:
CollectionViewSource viewSource_dataLinesUnfiltered;
CollectionViewSource viewSource_dataLinesFiltered;
...
public MainWindow()
{
InitializeComponent();
this.DataContext = dataLines;
viewSource_dataLinesUnfiltered = (CollectionViewSource)this.Resources["viewSource_dataLinesUnfiltered"];
viewSource_dataLinesUnfiltered.Source = dataLines;
viewSource_dataLinesFiltered = (CollectionViewSource)this.Resources["viewSource_dataLinesFiltered"];
viewSource_dataLinesFiltered.Source = dataLines;
// Control Events
this.ShowAA.RaiseEvent(new RoutedEventArgs(System.Windows.Controls.Primitives.ToggleButton.UncheckedEvent));
}
private void ShowAA_Checked(object sender, RoutedEventArgs e)
{
viewSource_dataLinesUnfiltered.View.Filter = null;
}
private void ShowAA_UnChecked(object sender, RoutedEventArgs e)
{
viewSource_dataLinesUnfiltered.View.Filter = delegate(object o) { return FilterContent(o as ErrorDetection.stDataLine, "AA", ""); };
}
bool FilterContent(ErrorDetection.stDataLine line, string sFilterAA, string sFilter)
{
shortArrayToHexStringConverter converter = new shortArrayToHexStringConverter();
string comBuffer = converter.Convert(line.ComBufferP as object,typeof(string),0,System.Globalization.CultureInfo.CurrentCulture) as string;
return !comBuffer.Contains("AA");
}
But I´m not sure why it works this way and the filter is not applied on window repaints when ICollectionView is used.
You need to specify which ICollectionVIew is used on which DataGrid.
If you just bind to the collection (dataLines in this case) WPF will use the 'default view' (or create one if necessary), this is why the first commented out line works for filtering.
There are a few ways you could specify which view is used for which datagrid, depending on what patterns, etc. you are using
1) Like the linked question, you could set the ItemsSource for each DataGrid in the window's code behind, after initializing the views, e.g.:
filteredDataGrid.ItemsSource = view_dataLinesFiltered;
unfilteredDataGrid.ItemsSource = view_dataLinesUnfiltered;
2) You could set the DataContext of the window to itself, or make a view model for the screen that contains the view, and make the views public properties and then bind to the intended view for each grid, e.g.
<DataGrid ItemsSource="{Binding View_dataLinesFiltered}"> ....
Edit:
Now I'm not at work and can get to dropbox and play with your example it seems like the cause of the weird behaviour is the use of CollectionView directly. On the msdn page for CollectionView it says
You should not create objects of this class in your code. To create a
collection view for a collection that only implements IEnumerable,
create a CollectionViewSource object, add your collection to the
Source property, and get the collection view from the View property.
However, if you don't want to set up the views in XAML, you could also change your CollectionViews to ListCollectionViews and it should work as expected (this is likely the view type that CollectionViewSource is making for you behind the scenes anyway).

What is the best way to know if the user has changed data in the DataGrid?

I would like to know every time a user modifies data in WPF DataGrid.
Is there a single event that I can use to do that? Or what is the minimal set of events that I can use to cover full set of data changes (Add row, delete row, modify row etc)?
I know that this is probably more than you are asking for, but once you do it, it's hard to go back. Whatever you are binding to ... some List, have that item implement IEditableObject.
that way you won't have to ever worry about whatever control/view implementation, events ets.
When the item is changed, the datagrid as well as plethora of .NET controls will set the IsDirty object to true.
These are not super great links but they will get you started thinking about maintaining isDirty flag.
https://msdn.microsoft.com/en-us/library/system.componentmodel.ieditableobject(v=vs.110).aspx
object editing and isDirty() flag
http://bltoolkit.net/doc/EditableObjects/EditableObject.htm
this is more what I am used to:
https://stackoverflow.com/a/805695/452941
Usually, when you are using MVVM, you bind the master list to an ObservableCollection and then the selected item to a specific instance. Inside your setters, you can raise events. This would be the most logical (read: the most common method I've seen) to capture updates / adds / deletes to a list of data.
MainWindow.xaml
<Window x:Class="WpfApplication.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication"
Title="MainWindow" Height="350" Width="525">
<DataGrid x:Name="dataGrid" AutoGeneratingColumn="OnAutoGeneratingColumn">
<DataGrid.Resources>
<Style TargetType="DataGridCell">
<EventSetter Event="Binding.SourceUpdated" Handler="OnDataGridCellSourceUpdated"/>
<EventSetter Event="Binding.TargetUpdated" Handler="OnDataGridCellTargetUpdated"/>
</Style>
</DataGrid.Resources>
</DataGrid>
</Window>
MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.dataGrid.ItemsSource = new ObservableCollection<Person>()
{
new Person() { Name = "John", Surname = "Doe" },
new Person() { Name = "Jane", Surname = "Doe" }
};
}
private void OnAutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
var dataGridBoundColumn = e.Column as DataGridBoundColumn;
if (dataGridBoundColumn != null)
{
var binding = dataGridBoundColumn.Binding as Binding;
if (binding != null)
{
binding.NotifyOnSourceUpdated = true;
binding.NotifyOnTargetUpdated = true;
}
}
}
private void OnDataGridCellSourceUpdated(object sender, DataTransferEventArgs e)
{
this.OnDataGridCellChanged((DataGridCell)sender);
}
private void OnDataGridCellTargetUpdated(object sender, DataTransferEventArgs e)
{
this.OnDataGridCellChanged((DataGridCell)sender);
}
private void OnDataGridCellChanged(DataGridCell dataGridCell)
{
// DataContext is MS.Internal.NamedObject for NewItemPlaceholder row.
var person = dataGridCell.DataContext as Person;
if (person != null)
{
var propertyName = ((Binding)((DataGridBoundColumn)dataGridCell.Column).Binding).Path.Path;
var propertyValue = TypeDescriptor.GetProperties(person)[propertyName].GetValue(person);
// TODO: do some logic here.
}
}
}
This is what I used for some complex DataGridCell formatting based on a Person (just some POCO) instance, property name and property value.
But if you want to be able to know when to save the data and you use MVVM, then the best way to do this would be to have original value and current value for every editable property in your view model / model. When data is loaded, original and current value would be equal, and if property is changed through DataGrid or any other way, only current value is updated. When data needs to be saved, just check if any item has any property that has different original and current value. If the answer is yes, then data should be saved, because it has been changed since last load / save, otherwise data is same as when loaded, so no new saving is required. Also, when saving, current values must be copied to original values, because data is again equal to the saved data, like when it was last loaded.
if you use mvvm you do not need to know when the "user modify data in the data grid" you have to know when the underlying collection change.
so if you use datatable(HasChanges/RejectChanges...) you have that all built in. if you use poco collections then your items at least have to implement INotifyPropertyChanged - if its raised the user modify data. Maybe IEditable is a good one too for reject changes and so on.

How to update a custom dependency property when the datasource list changes

We have a user control with a custom dependency property (DP). The DP is bound to an ObservableCollection.
When a new item is added to the collection programatically, the databinding does not update the target DP. Why? We think it's because, unfortunately, in our case the target is not a ListBox or ListView, but a Canvas. The DP, when changed or initialized, is supposed to draw a Shape (!) onto the Canvas, and the shape's position and size is bound to the collection item's two properties: WIDTH, LEFT.
Ideally we don't want to clear the Canvas and redraw all items just becasue one has been added (or deleted). But how?
So:
How can the custom DP take care of drawing the shape for the new collection item? What callback do we need, at what point in time does this have to happen, and what specific MetaDataOptions might there?
Also, are there any good resources out there concerning all these dependency property options. They are quite confusing. MSDN does not really help with what we're trying to do.
Thanks!
EDIT:
The ObservableCollection is like so:
public class Projects : ObservableCollection<Project>
{
//no ommitted code. this class really IS empty!
}
The DP is like so:
public class MyUserControl : UserContorl
{
public static readonly DependencyProperty... etc. typeof(Projects)
private static void OnProjectsChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MyUserControl u = d as MyUserControl;
CpProjectCollection projects = e.NewValue as CpProjectCollection;
u.Refresh(projects);
}
private void Refresh(CpProjectCollection projects)
{
foreach (CpProject p in projects)
{
//...add each project to the Canvas
ProjectContorl pc = new ProjectControl();
pc.Project = project;
Binding b = new Binding("StartTime");
b.Converter = new TimeSpanConverter();
b.Source = pc.Project;
b.Mode = BindingMode.TwoWay;
c.SetBinding(Canvas.LeftProperty, b);
//do the same for project duration
}
}
}
If you bind to ObservableCollection, you get the change notification if the collection is replaced with another collection, not when the collection's content is changed. So, you'll need to subscribe to CollectionChanged event in your code-behind.
If you subscribe to CollectionChanged, you can see which are the new/deleted items in your ObservableCollection. You can add a new shape for each new item and remove old shapes for deleted items.

Categories