I wanna use the PropertyGrid to modify a certain property of an object loaded from json. But in my situation, some properties are instances of a class, which also has it own properties.
I've tried to customize the editors to show those sub-properties:
<xctk:EditorTemplateDefinition TargetProperties="SystemParameter">
<xctk:EditorTemplateDefinition.EditingTemplate>
<DataTemplate>
<Button Command="ExpandCommand" CommandParameter="{Binding Value}">...</Button>
</DataTemplate>
</xctk:EditorTemplateDefinition.EditingTemplate>
</xctk:EditorTemplateDefinition>
var win = new MainWindow();
win.SetSelectedObject(JsonConvert.DeserializeObject(e.Parameter.ToString()));
win.ShowDialog();
It works but I cannot commit the modification to the first-level window. As a result, if one changed a sub-property, I cannot save this modification.
My question is:
How to use the [PropertyGrid] with cascade properties?
Is my solution right? If yes, how to save the modifications to sub-properties?
Related
In my MVVM Light application I do a search in a customer list. The search narrows the customer list which are displayed in a master/detail view with a datagrid (the master CustomerSearchResultView) and a separately defined usercontrol with FirstName, Lastname, Address etc, etc (the detail - CustomerSearchDetailView). Here are the main content of the master/detail view:
<StackPanel MinWidth="150" >
<TextBlock Text="Customer Search Result List" />
<Grid>
<DataGrid Name="CustomerList" ItemsSource="{Binding SearchResult}" SelectedItem="{Binding SelectedRow, Mode=TwoWay}" >
.....
</DataGrid>
</Grid>
<Grid Grid.Column="2">
<TextBlock Text="Customer Details" Style="{StaticResource Heading2}" Margin="30,-23,0,0"/>
<content:CustomerSearchDetail DataContext="{Binding SelectedRow}" />
</Grid>
</Grid>
</StackPanel>
Both have their corresponding ViewModels. Please remark the DC for the CustomerSearchDetail, SelectedRow - it is a property on the CustomerSearchResultViewModel and is defined like this:
private Customer _selectedRow;
...
public Customer SelectedRow
{
get { return _selectedRow; }
set
{
_selectedRow = value;
RaisePropertyChanged("SelectedRow");
}
}
...
Because of this I have not defined any DC on the CustomerSearchDetailView - it is set in the Binding on the "Master" view (as shown above) and it seems to work ok.
In my Model folder I have created the Customer class that is in use here. It implements ObservableObject and IDataErrorInfo and have public properties that raisepropertychanged events.
I run the application and everything seems to be ok. Note: the ViewModel for the CustomerSearchDetailView (that is CustomerSearchDetailViewModel.cs) is at this stage just an empty shell and not in use (as far as I can see ... the constructor is never accessed)
Now I want to add Save/Update functionality to my customer in the detail view. Ok, I add a Save button to the CustomerSearchDetailView like this:
<Button Content="Save" Command="{Binding Path = SaveCommand}" Width="80" Margin="0,0,15,0"/>
I create my "SaveCommand" RelayCommand property in my CustomerSearchDetailViewModel - but it is never accessed.
Hmmmmm ... well after some googling back and forth I come up with this:
<Button Content="Save" Command="{Binding Source={StaticResource MyCustDetails}, Path = SaveCommand}" Width="80" Margin="0,0,15,0"/>
I defined the "MyCustDetails" as a resource in this view pointing to the CustomerSearchDetailViewModel. And voila! I now hit the method when debugging ... but alas, my customer was of course "null". (In fact I spent 2 hours implementing the CommandParameter here and binding it to the "SelectedRow" Property on the master view - but the customer was still "null").
More googling and searching for mvvm examples, and I implemented my "SaveCommand" on the Customer class (the model object). And guess what? The edited customer got passed along - I could send it to my EF layer and everything seems to be ok ....
And - If you are still with me - here comes my questions:
1.) I would like - and thought that was the "proper MVVM way" of doing things - to have my CRUD/Repository accessing in the ViewModel. How can I do that in my scenario?
2.) Now that I have my CRUD in place via the Model class (Customer) - should i bother with question 1? In fact I have deleted the CustomerSearchDetailViewModel and everything runs ok. I feel I have invented the View - Model (MV) framework ... :-P
I would very much like feedback on this - and I apologize for this "wall of text".
Assuming DC means DataContext
Just my opinion:
First question is are you doing anything special with SelectedRow in CustomerSearchResultViewModel?
If the answer is no, just get rid of that property and have your CustomSearchDetailView bind directly to the DataGrid using {Binding ElementName=CustomerList, Path=SelectedItem}
Now your Save / update Commands need to be used by Button's in CustomerSearchDetailView. So instantly I'd be inclined to using a separate VM for that View and have these Command's defined there.
Now you mentioned these Commands were not accessed. Well the answer for that is because in your program you're never actually creating the CustomerSearchDetailViewModel.
Normal operation is your View's DataContext is it's VM(If it requires one. In your case you do imo cos you need it to hold your Commands)
looking at your code I'd guess your using MVVM Light. So in ViewModelLocator you have your Main property and in your Main View, you got the DataContext set using that Main property and Source={StaticResource Locator} where Locator is the ViewModelLocator created in App.xaml Resources. This thereby creates that ViewModel for that view defining that DataContext. You can ofcourse do the same in code-behind but let's not go off topic.
So in your case you got the DataContext set as SelectedRow which is of type Customer and Binding's are resolved using DataContext and that's why when your command's are defined in Customer it works fine but when it's in the VM it did not.
So why did it work when you had the commands in your VM and used
<Button Content="Save" Command="{Binding Source={StaticResource MyCustDetails}, Path = SaveCommand}" Width="80" Margin="0,0,15,0"/>
^^ That worked because the DataContext was not used since Source has been specified explicitly. and where-ever MyCustDetails was defined in resources, there the VM got created.
So it worked what's wrong with that?
Well it's quite a big mess. Also just like you mentioned Customer details in that VM was null. Well I hope you can guess why that was by now. It's because your VM was created in resources via x:Key="MyCustDetails" but nothing in it was ever used or set apart from when the Binding's referred to it explicitly
In this system we got commands that refer either to the Model which is plain wrong or the VM which is created as a resource just for this purpose. The DataContext is heavily linked to the "SearchResults" view making it not so easy for future extensions or layout updates.
If we keep the View <-> VM a 1 <-> 1 relattion we can avoid all this confusion. So in summary we can answer both your question's together. While this works, please don't let your code be like this and tweak it to better help expansion for future and comply with some basic guidelines.
So how do we do that?
Approach 1:
In your CustomerSearchDetail View, add a DependencyProperty of type Customer lets call this say SelectedCustomer.
Now replace DataContext="{Binding SelectedRow}" with SelectedCustomer="{Binding SelectedRow}" in CustomerSearchResultView
Now set the DataContext of your CustomerSerachDetailView as it's VM similar to how CustomerSerachResultsView links to it's VM(guessing through DataContext Binding in xaml using the ViewModelLocator)
Now you can have your commands in Button's of CustomerSerachDetailView just as <Button Command="{Binding SaveCommand}" ...
Finally because SelectedRow is no longer the DataContext of the CustomerSerachDetailsView, your Bindings for FirstName, Lastname, Address will all appear to stop working.
We got plenty of options to address this.
First is to in each Binding use a RelativeSource FindAncestor binding pointing to CustomerSerachDetailsView and there via the CurrentCustomer DP(DependencyProperty) we created before get the appropriate field.
eg:
<TextBlock Text={Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:CustomerDetailsView}}, Path=CurrentCustomer.FirstName}" />
now if you have multiple properties this is gonna soon start getting annoying to type. So then pick a common ancestor(say 3 of these TextBlocks are grouped under a StackPanel) and apply it's DataContext as the CurrentCustomer element via a similar binding to ^^. Now the StackPanel's children DataContext will be the Customer element so in each of their binding's you don't have to do the whole RelativeSource thing and can just mention {Binding Path=FirstName} and so on.
That's it. Now you got two view's with their own respective VM and a Model(Customer) and each have their respective tasks.
Great, we done? err not quite yet.
While Approach 1 is better than what we started with it's still just "meh". We could do better.
Approach 2
MVVMLight has a Messenger class that will allow you to communicate between different classes in a weak dependent format. You need to look into this if you haven't already.
So what do we do with Messenger?
pretty simple:
In the setter of SelectedRow in CustomerSearchResultsViewModel we'll send a message with the new incoming value to CustomerSearchDetailsViewModel.
Now in CustomerSearchResultsViewModel we'll add a property CurrentCustomer and assign it this incoming value.
In the CustomerSerachDetailsView we no longer create a DP. Which means we no longer set SelectedRow to anything(DataContext or DP) in the CustomerSerachDetailsView from CustomerSearchResultsView ( sweet less work :) )
As for the way we assign DataContext of CustomerSerachDetailsView or way we bind the Button.Command - They remain same as Approach 1
Finally the actual "FirstName" and so Binding's. Well now CurrentCustomer is a property of the CustomerSearchDetailsViewModel. So binding to it just like how the Button bind's to it's commands
^^ this works fine now cos DataContext for the TextBlock is the VM and the property CurrentCustomer exists in it.
I have an issue trying to understand how to implement the MVVM pattern in my application. It's a small application and I will explain what it does.
My application creates a backup of files. The UI lets the user choose which folder they want to back up and where it should be backed up too. After making their selection they click the "start" button.
This then passes the folder source and folder destination to a class library (called backup.cs) which creates the back ups of all the files inside each folder. During this, a log (Log.cs) is created logging each stage and the state of each file it attempted to back up (complete, failed, other, etc). Now, the log is in memory only.
When the back up is complete I want a window to open (a view) which will display all the logs. It's at this point I can't understand how to use the MVVM pattern.
As it stands today, I pass my log (which holds the data in a hierarchical way) to my MainWindow's constructor and bind to the datacontext, using a treeview in my xaml I get the desired results. However, I now want to use MVVM.
My question is very similar to my previous question, as you can see the answer is to pass the log as a paramter to the ViewModel constructor. The issue is, I don't know how to do that and also display a window!
The only way (in my head) I can achieve this is by passing the Log as a parameter to a constructor of my View but this defeats the point of the MVVM. I could pass the parameter to my ViewModel's constructor (which would fit the MVVM pattern) but would that then mean I have to also create an instance of my View from my ViewModel constructor as well? Otherwise all I would do is create my ViewModel but have no way to display the results since the View isn't displayed.
I hope I have explained where I'm struggling clearly, can any one suggest a way forward please?
Most likely you 'll want the viewmodel to accept (and expose through a property) a collection such as a List<Log> -- typically this would be an ObservableCollection<Log>, but if the operation has already completed there is no real point in going that way. This is what you are describing as a possible solution.
To wire the viewmodel to the view, in essence you need to do this:
var viewModel = new LogsViewModel(...);
var view = new LogsView(); // no constructor parameters
view.DataContext = viewModel;
And finally you add view at some place of the application window's logical tree so that it gets displayed. MVVM frameworks automate this procedure, but you can also do it manually as simply as this.
Your view would then bind to the logs collection to display each log, perhaps using a DataTemplate:
<ItemsControl ItemsSource="{Binding Logs}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<!-- XAML to display each Log does here -->
<TextBlock Text="{Binding FileName}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
As an example, if you wanted that LogView being shown based on a button click in you main View.
public override void ShowCommandExecute()
{
var popup = new LogsView
{
WindowStartupLocation = WindowStartupLocation.CenterScreen,
DataContext = new LogsViewViewModel();
};
popup.ShowDialog();
}
I have several MenuItems whose Commands are bound to my ViewModel. Until today, all of them execute properly.
Now I have added a MenuItem whose ItemsSource is bound to an ObservableCollection. The point of this MenuItem is to enumerate a list of plugins so that the name of all plugins show up. Then when the user clicks on a plugin name, it should call a function to display properties for audio filters.
In my current implementation, which doesn't work, I tried to databind like this:
<MenuItem Header="Filters" ItemsSource="{Binding FilterPluginNames}">
<MenuItem.ItemContainerStyle>
<Style>
<Setter Property="MenuItem.Command" Value="{Binding ShowFilterDialogCommand}" />
</Style>
</MenuItem.ItemContainerStyle>
</MenuItem>
The problem is that I get a BindingExpression path error because it's trying to use a String as the MenuItem's DataContext.
This leads me to believe that the DataContext for a MenuItem's MenuItems is automatically set to type of objects in the ItemsSource. Is this true?
If I need to change the DataContext, then I'd like to change it to the ViewModel that handles all of my other Commands. But if I do that, how in the world am I able to tell which plugin I want to display filter properties for? I'd need to pass in a CommandParameter at the very least, but binding this value to the filter name isn't my most favorite option. Are there any other ways to do this?
If the DataContext is indeed automatically set to the object type in the ObservableCollection, then I'd rather just call my interface method ShowFilterProperties() directly. I bet that I can't do this without Command binding. If that is the case, how do you all deal with this sort of application? Do you make all of the plugins expose a command handler, which will then show the dialog?
EDIT -- I modified my code to change the ObservableCollection type, and sure enough, WPF wants to databind to the type T. So I guess one option is to have the plugin expose the ICommand, but I don't know if this is a weird approach or not?
EDIT -- ok, I just learned something new. Interfaces can't have fields, so is it not possible to databind with plugins, period?
You are likely not quite binding like you think you are. You might want to just put some diagnostics on your bindings and see what object they are binding to. Here is a good link for debugging bindings:
http://www.beacosta.com/blog/?p=52
Here's a sample:
<Window …
xmlns:diagnostics="clr-namespace:System.Diagnostics;assembly=WindowsBase"
/>
<TextBlock Text="{Binding Path=Caption, diagnostics:PresentationTraceSources.TraceLevel=High}" … />
I think your approach is correct... it probably just needs to be debugged a little.
I'm working on a set of controls that has a number of DependencyProperties. The properties are themselves DependencyObjects and created during the get method of the properties. During the Get method, they are also set back to the propertybag using the SetValue() method, so they are in fact valid in Xaml and their properties can be storyboarded without having to explicitly created in the the visual tree.
These DependencyObjects has all its properties as DependencyProperties as well, for supporting DataBinding. They are as mentioned above possible to use in Storyboards.
At the same time I'm developing special designtime support for Blend 3 for these properties and have created an InlineEditorTemplate in the form of a Control. I create the template and set it for the PropertyValueEditor like this:
var vectorEditControl = new FrameworkElementFactory(typeof (VectorEditorControl));
var dataTemplate = new DataTemplate {VisualTree = vectorEditControl};
InlineEditorTemplate = dataTemplate;
In the Control I have the following:
<Grid DataContext="{Binding Value}">
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding Path=X, Mode=TwoWay}"/>
<TextBox Text="{Binding Path=Y, Mode=TwoWay}"/>
<TextBox Text="{Binding Path=Z, Mode=TwoWay}"/>
</StackPanel>
</Grid>
The editor shows up and I can edit the data. And even while debugging, I see that it actually sets the data back to the DependencyProperties on the DependencyObjects, but nothing happens to the Xaml. So the data is actually not persisted in any way in the Xaml and lost when I close the Xaml file and open it again.
Is there anything I need to do specifically for it to actually get into the Xaml? I was under the impression that this would happen automatically?
Excellent Question!
The core issue you're running into a misunderstanding as to what PropertyEditors in Blend/Cider end up databinding to.
Consider this object graph:
- MyControl
-- MyControl.MyProperty
--- FooClass
---- FooClass.BarProperty
Let's look at a scenario where we have a PropertyEditor (of any type: Inline, Dialog or Extended) to property MyControl.MyProperty.
When inside MyPropertyPropertyEditor you'd expect to get a fully settable copy of FooClass and be able to manipulate it's members.
That's a good assumption, but the wrong one.
The core issue is that Blend/Cider have elaborate data structures that represent your model at design time. There's about 3-5 levels of abstraction in how Blend/Cider interact with an actual control.
Creating those levels of abstraction allows Expression Blend / Visual Studio designers to be leveraged between framewroks (Silverlight / WPF) and support advanced scenarios (like Property transactions and property chaining).
So, the value you actually get to DataBind to is just one of those levels of abstraction.
Don't believe me? In your custom PropertyEditor, register for this.DataContextChanged event and checkout the type in this.DataContext. You'll end up getting the PropertyValue class (or one of it's friends).
Every single property change you want persisted to XAML (and shown on the design surface) should go through those abstraction layers.
the question you have to ask yourself is "Where do I get one of these absteaction classes for my PropertyValue.Value property instance?".
Well, what I'd do if I were you is create a ModelItem around MyControl.MyProperty and set that as your PropertyEditor.DataContext.
We've shipped an example of using ModelFactory.CreateItem in the Silverlight Toolkit as part of the Chart DefaultInitializer: Source Code, Ning Zhang (Awesome Design Time Dev) explains about ModelItem
If you've got follow-up questions I'd consider pinging PeteBl or UnniR through the Silverlight Insiders mailing list.
Sincerely,
-- Justin
It partly solves my problem. I'm having a dialog with UnniR for a followup.
I couldn't see how I could use this together with the PropertyValueEditor, but for default values this is brilliant and something I'll implement ASAP.
Thanks.
I'm trying to change the content of a ContentPresenter to one of my View Model class. I manage to have it shown properly, once I change this content (property) from my model, it doesn't update the ui.
The following lines link my view model classes to their respective ui (setting their data context automatically) :
<DataTemplate DataType="{x:Type vm:WelcomePageViewModel}">
<vw:WelcomePage></vw:WelcomePage>
</DataTemplate>
<DataTemplate DataType="{x:Type vm:UnitPageViewModel}">
<vw:UnitPage></vw:UnitPage>
</DataTemplate>
I want to show the ui using binding:
<ContentPresenter Content="{Binding CurrentChildViewModel}" />
So in my ViewModel class, I have a CurrentChildViewModel property, which is an Instance of a closableViewModel. I first assign it in the constructor.
But now when I change the value of my CurrentChildViewModel, it won't update the ui, even if the property is changed in the viewmodel, the first element assigned will stay.
I don't see what I'm doing wrong here. Maybe I'm not using the right architecture (method) to change the content of an element.
Your help would be much appreciated.
Thanks in advance,
Boris
Make sure to implement INotifyPropertyChanged. The data binding framework doesn't constantly "poll" for changes to the bindings, but relies on the property change framework to fire off the updates for bindings.