Applying ContentDefinitionEditorEvents only to a specific field type

Topics: Writing modules
Apr 4, 2011 at 1:08 PM
Edited Apr 4, 2011 at 1:31 PM

Hello. I am currently learning how to create new field types in Orchard. I've read the tutorial and several custom field modules from the gallery, and still have difficulty addressing the following problem when defining 'field settings' part:

I can specify a 'driver' for my settings (not a real contentPart driver, but conceptually the same) , by defining

public class MyFieldEditorEvents : ContentDefinitionEditorEventsBase

This driver will be called when the 'type constructor' page loads. For EVERY field in the content type or content part. This is perfectly fine and similar to  other drivers and handlers in Orchard. Since I want to handle only my custom field type, I can add a check for field type name:

public override IEnumerable<TemplateViewModel> PartFieldEditor(ContentPartFieldDefinition definition)
        {
            if (definition.FieldDefinition.Name == typeof(MyField).Name)
            {
             var model = ...
             //bind and display my model
             yield return DefinitionTemplate(model);
            }
        }

But the 'update' method does not provide me with information about field definition, contentpart definition or contentItem definition that is being updated.

public override IEnumerable<TemplateViewModel> PartFieldEditorUpdate(
            ContentPartFieldDefinitionBuilder builder, IUpdateModel updateModel)
        {
            var model = new MyFieldSettings();
            if (updateModel.TryUpdateModel(model, ModelPrefix, null, null))
            {               
//update settings here
builder.WithSetting(FieldStorageProviderSelector.Storage, "MyStorageProvider");
            }

            yield return DefinitionTemplate(model);
        }

Is there any way to get fieldDefinition in this point to apply my driver's functionality only to my field type? In particular, I am trying to supply a custom FieldStorage for my field type. And currently the 'storage' setting is being applied to all fields of the ContentType that is being edited. Of course, I could add some fake text property to my settings model and template, and check if 'updateModel' (that actually points to controller's 'TryUpdateModel' that reads data from Request) has found it, but this looks like a loosy walkaround. Is there a better solution? The tutorial about writing custom field types seems to ignore this problem and simply applies settings to all fields.
The Orchard.ContentManagement.Builders.ContentPartDefintionBuilder.FieldConfigurerImpl class that is actually passed as 'builder' parameter at runtime (if no modules change that), DOES in fact contain a reference to ContentFieldDefinition, but only as a private field :( And reflecting on a private field of a type that can be changed by dependency injection from other modules, would be even worse solution.
Apr 4, 2011 at 1:18 PM
Edited Apr 4, 2011 at 1:25 PM

Ups, spend 10 more minutes and found the answer myself. ContentPartFieldDefinitionBuilder has 'OfType(string)' method that allows to make field type checks. Well, perhaps my post will help somebody else (because the method cleverly hides near common 'object.GetType' in intellisense).

[edit]

No, wrong again :( The Orchard source code never stops to confuse me. 'OfType' method is actually used to set 'ContentFieldDefinition' property of the builder during construction (ironically, this means that a 'setter' for ContentFieldDefinition is public, while the 'getter' is private...).

Still no solution.

Apr 4, 2011 at 2:53 PM

I think this is by design. A field's settings apply to every content item it's attached to, otherwise I guess you'd be mostly storing loads of redundant information.

Maybe what you need is a ContentPart which will give you far more flexibility. You could maybe create a ContentPart that would change storage for all fields on the given content item (although I have no idea how you'd hook in to actually change how the fields are stored in this case).

Or you could just create a ContentPart to handle all your data instead of using fields.

Apr 4, 2011 at 3:42 PM
Edited Apr 4, 2011 at 4:26 PM

Yes, of course the settings are the same for all ContentItems. They are set on 'per field' basis. But I am talking about ConentTYpes and ContentParts what fields are added to, not ContentItems that store field values. Custom field settings can be set for individual fields in all contentTypes and all contentParts what the field is added to. The real problem is they're applied to all fields, no matter the field type.

For example, a contentType has two fields, one of default type 'TextField', another of my custom type 'MyField'. When I edit this contentType settings, my MyFieldEditorEvents:ContentDefinitionEditorEventsBase driver will be called twice (once for each field). When rendering 'display' the type check that I wrote (if (definition.FieldDefinition.Name == typeof(MyField).Name)) will trigger, and the shape for editing custom settings will be added only for 'MyField' field. If I did not make this check in PartFieldEditor method, my custom settings would also be displayed for 'TextField' field, even if this field does not need any settings.

But in PartFieldEditorUpdate I can not check field type, so the 'update' of my custom settings will happen for both fields. If the settings are just a couple of string values, its not a problem (since the custom editor shape was not rendered for TextField, there will be no values in the HttpRequest and nothing wrong will be written to TextField.Settings). But if updating settings involves a bit more complex logic, such 'triggering settings update for the wrong field type' can break something. In my example above, changing FieldStorage for MyField type also changes FieldStorage for TextField type (in fact for all fields ever created, unless their own driver also tries to set FieldStorage), that is completely unasked for.

Another example. If a 'DateTimeField' module shown in 'creating a custom field' tutorial is added to any orchard site, ALL other fields of ANY type that are created or updated will receive 'DateTimeFieldSettings.Display=DateAndTime' setting. Because 'builder.WithSetting("DateTimeFieldSettings.Display", model.Display.ToString());' line from the tutorial will trigger for ALL field types (not only 'DateTime' fields), and model.Display defaults to 'DateAndTime'. Even basic 'TextField's will now receive those settings. Of course, other field types except for 'DateTime' will probably simply ignore 'DateTimeFieldSettings.Display' key in their settings, but it is still strange and wrong to have such 'orphan' settings. And in cases of conflicting key names (such as 'Storage' key that define storage provider for any field type) - can lead to some odd behavior.

P.S. By 'field' I mean 'named property of a ContentPart/ContentType', as stored in 'Settings_ContentPartFieldDefinitionRecord' DB table, not a field type or field value that is individual for each contentItem.

Apr 4, 2011 at 6:27 PM

At first I thought this sounded like a bug. But I'm using the Orchard Taxonomies module, and in that I've added two TaxonomyFields to a content type, each with different settings, and it works fine.

In any case I think what you're trying to do is out of the scope of implementing in a field. You're trying to create a field that changes the storage method of a field. So how is the system supposed to know where to look for the field's storage... Surely it first has to load the field from default storage before it can know its settings.

I think you just need to create a separate content part and implement your storage scenario there.

Coordinator
Apr 4, 2011 at 7:09 PM

Any field driver can provide information on any field type. This is an intended behavior, and it's used for instance by the Indexing module to add a setting on any field, to check if a field should be part of the index. Filtering on the field definition is good if you want to specifically target a field type. We are investigating on the settings storage you are suggesting, which happens on every field, to see if it is a bug or not.


Apr 4, 2011 at 7:16 PM

Wait ... I hadn't quite realised that ... so it's actually ridiculously straightforward to add DisplayName as a setting on all fields. I went about it the hard way...

Apr 5, 2011 at 11:01 AM
Edited Apr 5, 2011 at 11:03 AM
randompete wrote:

At first I thought this sounded like a bug. But I'm using the Orchard Taxonomies module, and in that I've added two TaxonomyFields to a content type, each with different settings, and it works fine.

There is still some misunderstanding. The framework allows one contentType to have two fields of the same type with different settings. The problem is with settings 'driver' (IContentDefinitionEditorEvents) being aplied to all fields without ability to filter by a specific field type.

And by 'FieldStorage' I mean not 'storage of field settings' but 'IFieldStorage' and 'IFieldStorageProvider' interface implementations. By default it is responsible only for serialization of field values to XML. And default 'SimpleFieldStorage' storage class uses 'Convert.ToString()' an 'Convert.ChangeType()' for serialization/deserialization, so it is very limited on the number of data types that it can handle. I wrote my own implementation that uses XmlSerializer and can handle more data types (including arrays). But the only way that I could 'plug into' the system was to set 'Storage' setting for all fields of a specific type.

sebastienros wrote:

Any field driver can provide information on any field type. This is an intended behavior, and it's used for instance by the Indexing module to add a setting on any field, to check if a field should be part of the index. Filtering on the field definition is good if you want to specifically target a field type. We are investigating on the settings storage you are suggesting, which happens on every field, to see if it is a bug or not.

Thanks for the responce. 'Targeting a specific field type' is exactly what I am trying to do. The problem is I can do it in 'display' method of the settings driver, but can't check field type in 'IContentDefinitionEditorEvents.PartFieldEditorUpdate' method because I do not have access to ContentFieldDefinition.

And regarding setting field storage - as I wrote above, I intended to alter the serialization used for storage of field values to suport some complex data types (arrays in particular). The only 'hook' that I've found was to set 'Storage' setting for my fields. (Perhaps there is another, better hook that I've missed?) And it works fine, I was able to serialize arrays and many other types that XmlSerializer could handle. The only problem is my 'settings driver' tries to update 'Storage' for all field types, and i can't filter by field type in driver's 'update' method.

I've used a walkaround, bit still it would be usefull to have access to ContentFieldDefinition in IContentDefinitionEditorEvents.PartFieldEditorUpdate method. And I think the tutorial for custom field creation (and custom field module currently in the gallery) should be altered to check field type on 'updates' and apply 'DateTimeFieldSettings.Display' setting value only to fields of 'DateTimeField' type. It is really unexpected to get this setting on all text fields after enabling the 'DateTimeField' module.

Again, thanks for reviewing my case. It is not actually a bug, just a minor unconvenience in interfaces. If ContentPartFieldDefinitionBuilder class exposed ContentFieldDefinition as public property or method, there would be no problems. Currently it allows to SET ContentFieldDefinition (via OfType() method), but does not allow to GET it.

May 24, 2011 at 12:54 PM

I think this clearly is a bug which could be solved with 2 lines of code inside class ContentPartFieldDefinitionBuilder:

1. add property: public string Name{ get; private set; }

2. initialize property from within constructor: this.Name = field.FieldDefinition.Name;

 

Exactly the same way as it is handled within ContentTypePartDefinitionBuilder. And the PartFieldEditorUpdate method would start like all other ContentDefinitionEditorEventsBase.XyzUpdate methods:

            if ( builder.Name != "DateTimeField" )
                yield break;

 

BTW: IMHO PartFieldEditor(Update) really should be named TypePartFieldEditor(Update) to be consistent to PartEditor(Update) and TypePartEditor(Update).

Coordinator
May 24, 2011 at 8:42 PM

You should file a bug then.