This thread looks to be a little on the old side and therefore may no longer be relevant. Please see if there is a newer thread on the subject and ensure you're using the most recent build of any software if your question regards a particular product.
This thread has been locked and is no longer accepting new posts, if you have a question regarding this topic please email us at support@mindscape.co.nz
|
Hi, I am binding the Selected object of the property grid to an object - for which one of its properties has a TypeConverter. The trick is that the GetStandardValues call absolutely requires the context to be set before it can determine what are the Standard values to display - i.e. it's not a global thing. The problem is that the context always seems to be null, so neither the property value, nor the list of StandardValues is displayed in the resulting property grid. I believe that the context.Instance is supposed to contain the selectedObject (assuming I understand all this correctly). The object that we are using does display correctly in a WinForms property grid, so I don't think there is anything wrong with the object itself. Thanks |
|
|
Just a quick clarification, the property value does display. That was a mistake. This is super easy to reproduce, even a standard implementation of a TypeDescriptor will work, just override GetStandardValuesSupported and return true, override GetStandardValuesExclusive and return false. and override GetStandardValues(ITypeDescriptorContext context) I think that's it. It's not even necessary to implement GetStandardValues properly, just breaking in the method will show that context is null. |
|
|
You are correct that we do not provide an ITypeDescriptorContext. I've had a look at the code to see whether this would be feasible and unfortunately it doesn't look very promising -- we don't have the instance context (which I believe is what you want) at the point we call GetStandardValues, and I don't see any quick fix that would get it there. I will log a bug for this because it would certainly be nice to improve our compatibility with existing converters this way. In the meantime, depending on your scenario, you may be able to use a PropertyEditor or TypeEditor rather than a TypeConverter. A PropertyEditor has access to the object on which the property is exposed (the SelectedObject or, for hierarchies, possibly a subobject) via {Binding UnderlyingObject}. I believe you could use that with a suitable value converter to populate a combo box. You should be able to reuse your TypeConverter code or even call the TypeConverter explicitly to avoid any duplication. |
|
|
Wow this is not obvious at all! After hours of frustrating debugging I finally got a custom property editor to work, but it was so incredibly painful I'm not sure it was worth the effort. The PropertyGrid SelectedObject is bound to a property of my ViewModel. This property is actually an abstract class that could be implemented by any number of possibly implementations. The problem with the TypeConverter is happening on a sub-property of one of the implementations of the abstract class. Whether this sub-property actually exists or not depends on the implementation of the abstract class. So having a propertyEditor for this is ridiculous. However, I'm not sure a type convertor works either, because the available Values for this property is directly dependant on other properties for the object, and so are not just type dependant. Anyway, ignoring all the rightness or wrongness (sic) of implementing a custom editor, the only way I could get the binding of the ItemsSource to work was to bind to a property in the viewmodel. The problem with that is that now I have write a whole slew of logic in the viewModel to figure out if the list needs to change. And this is really DISGUSTING, because there is no way the viewModel needs to know any of this. Not to mention that the only way to actually get the binding to work is to delay set the binding of the ItemsSource in the ComboBox_Loaded callback and use code to do it (see below). Perhaps there is another way, but I couldn't find it. Once your in a dataTemplate - forget it, life becomes very difficult. private void ComboBox_Loaded(object sender,RoutedEventArgs e) So needless to say this is a huge hack. I can't wait for my code review :( |
|
|
Have you got a simple sample that demonstrates what you're trying to achieve (including where the dependencies are between the SelectedObject and the source of the AvailableValues data)? We'd be happy to take a look at it and see if there's a simpler way to go about it. Do you still feel that having the selected object in the ITypeDescriptorContext would solve the problem? If so, can you describe how it would be different from using the UnderlyingObject in the property editor? Again, understanding where the AvailableValues are meant to be coming from would help us to advise here, and if the ITypeDescriptorContext solution is really going to make a significant distance then we can try to bump it up the priority list. The need to delay binding seems a bit weird -- from your code you should just be able to write <ComboBox ItemsSource="{Binding AvailableValues}" /> It sounds like that didn't work -- what went wrong? Binding errors, wrong result, nothing at all? |
|
|
Just to answer the easy question first, I did try that binding and got the following error: BindingExpression path error: 'AreaCodeList' property not found on 'object' ''ObjectWrapper`1' (HashCode=58888299)'. BindingExpression:Path=AreaCodeList; DataItem='ObjectWrapper`1' (HashCode=58888299); target element is 'ComboBox' (Name=''); target property is 'ItemsSource' (type 'IEnumerable') This error is generated from the test project I included in the post - although I commented that out in the uploaded version, but feel free to play with that. Ok, so the project I uploaded is essentially the code I am trying to run, although it is new code, and not code from my actual product which would have got me shot :) Granted the example is a bit stupid, but it was the only thing I could think of that even remotely made sense. For the sake of argument I coded the same kind of class structure - with a viewModel, an abstract model, and a derivedModel. Keep in mind that while the example only has one derivation in our real product we have many derivations of both model, and modelProperties. And just to be a real trouble maker, our customers can implement their own derivations so we really have no way of knowing what could be in there. Which is why the UI really needs to be generic and not implement custom editors. IThe UI shows 3 property grids, the first with no custom editors, the second with a custom property editor, and the third is a WinForms property grid. I thought the third might be useful so that you can see how the standard control calls the TypeConverter. The only difference I can see just from debugging is that your control calls the TypeConverter when the form loads, and the WinForms one calls it only when the comboBox is opened. Last thing, I actually have not solved all my problems even with the changes I've made. When the City property is changed, the AreaCode property is updated and the AvailableValues list is regenerated. The problem is that the comboBox.SelectedValue property doesn't refresh properly, because for an instant the SelectedValue is not in the Items List. ARGH. Perhaps a ComboBox is not the best choice. I was going to try your DropDown control to see if that helps, but I haven't tried that yet. Sorry for the crazy example, I really did simplify the code enormously. You should see our real code :) |
|
|
Crazy is fine. I'll take a look at that asap (I'm heads down on something else right now but hopefully later today or tomorrow). Regarding the binding error, if AreaCodeList is a property on the same object as the property you're editing, try this: ItemsSource="{Binding UnderlyingObject.AreaCodeList}" ObjectWrapper<T> is the class that acts as the data context for property editors: in addition to the Value property (which is what you normally bind to) it also provides UnderlyingObject which allows you to get back to the object whose property is being edited, and Property which allows you to get at the property metadata. Both of these can be useful when the editor needs context as in your case -- they won't solve your concern about making the viewmodel messy, but they might make setting up the plumbing a bit easier. |
|
|
Ha interesting. It still doesn't work, but I think it kind of makes sense. New Error: BindingExpression path error: 'AreaCodeList' property not found on 'object' ''DerivedModelProperties' (HashCode=47980820)'. BindingExpression:Path=UnderlyingObject.AreaCodeList; DataItem='ObjectWrapper`1' (HashCode=58888299); target element is 'ComboBox' (Name=''); target property is 'ItemsSource' (type 'IEnumerable') So the class it's giving me now is the DerivedModelProperties class, which makes sense because that is the SelectedObject of the propertyGrid. However, that doesn't help me. The AreaCodeList is on the viewModel class which is the DataContext of the Window, the DerivedModelProperties is a property of this ViewModel class. Although, I suppose theoretically I could move it to this class assuming there is a way to tell the propertyGrid not to display this property in the list. Does the BrowsableAttribute do that? The only other thing is that the Model engineer will not be happy. As long as the cludge is on my side, it's less of a problem :) |
|
|
Yes, we do respect BrowsableAttribute. But I agree that you shouldn't corrupt your model to adapt to the behaviour of a specific control. You could probably do a RelativeSource/Ancestor or ElementName binding to get at the Window, then traverse down through the DataContext: ItemsSource="{Binding DataContext.AreaCodeList, RelativeSource={RelativeSource AncestorType={x:Type Window}}}" (syntax may not be bang on but you get the idea) |
|
|
That's a little better. So now the DataTemplate looks like this: <DataTemplate x:Key="AreaCodeEditor"> Slightly random question. As you can see I added IsEditable="True" to the combo box. In this way the user can edit the currentValue of regardless of what is available in the drop down. Is there a way get this type of behaviour without providing a custom editor? Perhaps globally. I tried this, <ms:TypeEditor EditedType="{x:Type core:AreaCode}" But it doesn't work, I get the following errors: System.Windows.Data Error: 39 : BindingExpression path error: 'Value' property not found on 'object' ''AreaCode' (HashCode=6887897)'. BindingExpression:Path=Value; DataItem='AreaCode' (HashCode=6887897); target element is 'ComboBox' (Name=''); target property is 'SelectedItem' (type 'Object') |
|
|
Okay, here's how to do this with a value converter. First, how do we get the area codes from the selected object? That's easy: the logic is already in the type converter. Second, how do we get the selected object in the data template? As discussed, we can get this from {Binding UnderlyingObject}. So we can implement a value converter that basically just calls the type converter, and pass the underlying object to that value converter. But now we run into the problem that the values don't update. This is because the UnderlyingObject is the same object even though the Value is changing. We need changes to Value to trigger the binding to update. So... Third, we implement an IMultiValueConverter that receives both the underlying object (which never changes) and the value (which does change). The IMVC will throw the value away, but having it there does suffice to trigger the binding. So our multivalue converter looks something like this: public class AreaCodeExtractor : IMultiValueConverter Here FakeTypeDescriptorContext is a trivial implementation of ITypeDescriptorContext that just wraps an object and returns that object from its Instance property. Alternatively of course you could extract the logic from the AreaTypeCodeConverter and pass the instance directly -- I just did it this way to isolate the new code and avoid changing your existing code. But the ITDC approach has a potential side benefit I'll mention at the end. With this converter written, we can now rewrite the DataTemplate as follows: <DataTemplate x:Key="AreaCodeEditor"> And now as we update the City, the collection of available area codes updates as well. And you can now remove the ViewModel bits relating to updating the area code list, keeping your ViewModel nice and clean. (And I realise the multi value converter trick is not going to win the Martin Fowler Prize for Design Excellence, but it is at least a relatively encapsulated piece of kludgery.) I've attached a zip file of the changes I made to get this working. I lazily put my converter in the Window1.xaml.cs file but in reality you would of course create a separate file for it -- it's completely independent on Window1. Let me know if you have any problems getting it working. As a final note, you may think, "But wait, since I can also get the property metadata in the DataTemplate, I could pass that into the IMVC as well, which could then figure out which TypeConverter to use. So I could write a generic StandardValuesConverter that would work on any property that has a TypeConverter, and use that in a generic data template." Yes, and this is where the FakeTypeDescriptorContext trick plays off: instead of extracting logic from each type converter and creating a corresponding value converter, you can pass the FTDC to a type converter determined at runtime by the generic value converter. But now how do you associate the generic data template with the right properties? We have a trick for this called "smart editors," discussed at http://www.mindscape.co.nz/blog/index.php/2008/04/30/smart-editor-declarations-in-the-wpf-property-grid/ and http://www.mindscape.co.nz/blog/index.php/2008/12/11/smart-editors-for-the-wpf-property-grid-meet-smart-templates/. You don't have to do this, of course: if you have only a few properties that need contextual standard values then it's probably not worth the additional learning -- I mention it merely as a technique that you may want to investigate if you have a lot of contextual type converters. |
|
|
TypeEditors have two modes: value editing and reference editing. In value editing mode, your data template gets an ObjectWrapper<T> so you can use the Value, UnderlyingObject and Property bindings. In reference editing mode, your data template gets the property value itself (the AreaCode object) as its data context, so you can access only the members of the AreaCode class. By default, type editors for value types (structs) get value mode, and type editors for reference types (classes) get reference mode. Your AreaCode type is a class, so your TypeEditor is using reference mode. So you could bind to and edit the CountryCode and LocalCode, but you can't replace the AreaCode instance with a different one, because you don't have the Value property. (See the PhoneNumber editor example in the samples for an example of how reference TypeEditors are used.) You can override the mode by setting TypeEditor.TemplateBindingMode. If you set this to WrappedValue you will get the value mode behaviour, i.e. the ObjectWrapper<T>, and can use the Value, UnderlyingObject and Property bindings again. (PropertyEditors are always value mode, which is why you got the Value, UnderlyingObject and Property bindings in your original editor even though it was editing a reference type.) |
|
|
Phew! Firstly, I implemented your suggestion with the FakeTypeDescriptor. As advertised it does the trick - although you're right, I'm not sure I would give this approach any code cleanliness awards. However, the patch is fairly isolated, which is much better than we had before. I didn't have a chance to explore the "generalization" of the patch. I'll try to get to that next week. The only remaining problem is the ComboBox itself. As I think I mentioned a few posts before there is a refresh problem. When the City property is changed, the subitems need to change in the AreaCode available options. However, the SelectedValue of the AreaCode property changes before this happens. So the ComboBox is put into a state of confusion. We are trying to set a SelectedValue that doesn't exist in the list of items. PROBLEM. This results in no items being selected at all, and we have an empty field in our property grid. The only way I could think of to overcome this problem is to override the ComboBox class as follows: public class DerivedComboBox : ComboBox
The purpose being that once the Items list is refreshed we want to maintain the selection. So, if the SelectedValue is null then we'll select the first item in the list. However, only if the user hasn't decided to type in something different, whereas we leave it the way it is. Sadly, this doesn't entirely fix things either, because there is no guarantee that the item we want to auto-select is the first one. In fact I can tell you in our product it could be the first or the last. But, the main thing is we don't have any "visible" bugs, |
|
|
Regarding the TypeEditor question, you're right of course, that was the issue. Although, how do you get the cool styling? Mine just looks like the boring comboBox. |
|
|
You can find the combo box (and other) styles in Blue.xaml, which is in the installation folder under the Source / Styles folder (you don't need a source code licence for this). |
|