How to access content item fields when you can't hard-code the field name?

Topics: Core, Customizing Orchard, Writing modules
Developer
Jan 17, 2013 at 4:52 PM
Edited Jan 17, 2013 at 4:52 PM

Hi!

I'm building a part where I need to access a field on content items, where the field name is dynamically configurable in the part settings. I've been banging my head against the wall for 3 hrs now on how to do it.

According to Bertrands post on Clay property syntax and indexer syntax are equivalent:

somePageContentItem.Page.SomeField

should be the same as

somePageContentItem["Page"]["SomeField"]

However this does not work. The former syntax works fine, but the latter syntax yield a RuntimeBinderException "Cannot apply indexing with [] to an expression of type 'Orchard.ContentManagement.ContentItem'".

What is the right way to do this? I've really tried to dissect the object graph to find a way and must have combed through half of Google, but alas... :)

Thanks!

Coordinator
Jan 17, 2013 at 5:07 PM

Not all Clay objects have all the behaviors attached to them. Also, you might want to move away from Clay as it's being removed from 1.7.

Developer
Jan 17, 2013 at 5:09 PM

OK, good advice. But how can I accomplish what I'm trying to do, i.e. access a field whose name I can't know at compile time?

Developer
Jan 17, 2013 at 5:17 PM
var field = somePageContentItem.Page.Fields.Where(f => f.Name == "SomeRuntimeKnownFieldName").Single();

Make sure that somePageContentItem is not dynamically typed, but statically to be able to use LINQ.

Developer
Jan 17, 2013 at 5:27 PM
Edited Jan 17, 2013 at 5:27 PM

Don't think that would work. If somePageContentItem is not dynamically typed, then the .Page accessor cannot be used.

I could do this with a statically typed somePageContentItem:

var fieldQuery =
  from p in somePageContentItem.Parts
  from f in p.Fields
  where f.Name == "SomeRuntimeKnownFieldName"
  select f;

var firstField = fieldQuery.First();

But this only gets me an object of type ContentField. How do I get the field value from this object? It only seems to contain field definition information.

Developer
Jan 17, 2013 at 5:35 PM

Check out Contents Libraries from Helpful Libraries. There's one method to retrieve a field from a content item just by its type (not name).

Coordinator
Jan 17, 2013 at 5:49 PM

It depends on the object, but in your case, a little bit of reflection, together with the Get method of ContentItem, and then the Fields collection on ContentPart should do the trick, without having to go through dynamics.

Developer
Jan 17, 2013 at 6:05 PM
Decorum wrote:

Don't think that would work. If somePageContentItem is not dynamically typed, then the .Page accessor cannot be used.

 


Excuse my error, you're right of course. You would need to use Linq to access the part as well. Or, use a combination of both:

var field = ((ContentPart)somePageContentItem.Page).Fields.Where(f => f.Name == "SomeRuntimeKnownFieldName").Single();

Now somePageContentItem does need to be dynamically typed, accessing its Page part, which in turn is cast to ContentPart, so you can access its Fields using LINQ.
And as you already did in your example, you can access Parts using LINQ as well and not use dynamic at all.

In theory you should be able to access the Value field by invoking Storage.Get<string>(). However, not all fields may implement a Value property nor implement it like that, so precaution is advised.

Developer
Jan 17, 2013 at 7:18 PM
Piedone wrote:

Check out Contents Libraries from Helpful Libraries. There's one method to retrieve a field from a content item just by its type (not name).

Very convenient stuff - will try it out, thanks!

Developer
Jan 17, 2013 at 7:22 PM

@bertrand: Yes, getting to the ContentField object I understand, but then getting the actual value through that API seems less obvious... will try sfmskywalker's suggestion, see below.

@sfmskywalker: Makes sense, I guess I could do it either way to get to the ContentField. Your last paragraph is the piece of the puzzle that I ammissing I think, I will try it out. In this case I know up-front that I am always dealing with a ContentPicker field, so depending on how that field type handles its persistence I should be ok.

Developer
Jan 17, 2013 at 7:31 PM

If you know beforehand the content field type you may just as easily cast the field to that type and access the properties of it, such as "ContentItems"

Developer
Jan 17, 2013 at 7:37 PM
sfmskywalker wrote:

If you know beforehand the content field type you may just as easily cast the field to that type and access the properties of it, such as "ContentItems"

True, but to do that I would need to have a reference to the ContentPicker module if which that type is defined, which I would rather avoid.

I tried your suggestion theField.Storage.Get<int[]>("Ids") but I can't get it to return anything other than null. I have tried both "Value", "ContentItems" and "Ids" as the argument to the Get method, but it always just returns null. Any idea what it expects as the "name" argument?

Developer
Jan 17, 2013 at 7:39 PM

Hey wait, just realized I can do this:

((dynamic)theField).Ids

And it works! ;) 

Thanks for all the help guys!

Developer
Jan 17, 2013 at 7:56 PM
Decorum wrote:

I tried your suggestion theField.Storage.Get<int[]>("Ids") but I can't get it to return anything other than null. I have tried both "Value", "ContentItems" and "Ids" as the argument to the Get method, but it always just returns null. Any idea what it expects as the "name" argument?

I think the ids are stored as a comma separated string, so this should work: theField.Storage.Get<string>("Ids").

But yeah, casting theField to dynamic works as well :)