Showing posts with label c#. Show all posts
Showing posts with label c#. Show all posts

Tuesday, July 17, 2012

Validating content on Save - Part 1 of many

Recently I had to do a project where we are validating quite some content options whenever the editors decide their work is done. This got me thinking about sharing some of these experiences. This post is the first of a few that I have lined up on this subject, and where I try to cover the available options for content validation from the simple (and cheap) ones to the very complex and over-engineered.

For (many) years, the solution to the eternal "how do I validate content" question with Tridion has been a combination of making fields mandatory and using the Event System (OnComponentSavePre anyone?).
Recently I had to take a different approach to this, since Tridion 2011 brings all this amazing extensibility framework (codenamed Anguilla) that allows to do whatever we want with our content, right? While this is true, the correct answer comes in many shades of gray, and the biggest obstacle on creating a great solution for validating content for your editors is the learning curve.

Let’s think about how we can do this, by taking a very simple example, then growing it out to bigger things. In the first scenario we're covering in this series of posts, we will focus on how to validate that a field named "Title" starts with a capital letter, and we'll do this using 3 different approaches:
  • Schema restrictions
  • Event System
  • CME Command Extension
Using A Schema Restriction As described in LiveContent, you can apply XSD restrictions to simple Tridion Web Schema fields, so we could change the field definition to be something like this:
<xsd:element maxoccurs="1" minoccurs="1" name="Title">
    <xsd:annotation>
        <xsd:appinfo>
            <tcm:extensionxml xmlns:tcm="http://www.tridion.com/ContentManager/5.0"></tcm:extensionxml>
        </xsd:appinfo>
    </xsd:annotation>
    <xsd:simpletype>
        <xsd:restriction base="xsd:string">
            <xsd:pattern value="[A-Z][A-Za-z0-9_ ]*"></xsd:pattern>
        </xsd:restriction>
    </xsd:simpletype>
</xsd:element> 

This will now force our field to start with an upper case letter, and Tridion will enforce the rule.

 If I now try to save my content, Tridion will let me know I don’t understand what I’m doing:

And since I won’t understand the error message either, this becomes more complex than what we can handle with simple schema restrictions…
There are other simple scenarios, particularly with number fields, that Tridion handles quite gracefully (by not allowing the editor to type a number higher than specified in "xsd:maxInclusive" for instance), or limiting the maximum number of characters you can type in a field. However, fairly quickly we fall into a domain where business logic is required to validate fields (common examples are Start and End Dates for events, where you depend on 2 fields to provide the correct validation).

So this was the "cheap" content validation: schema restrictions.

Let’s go now to the 2nd option, using the Event System.

Taking the same example from above (first character must be upper case) we could take one of 2 approaches: Let the user know that the first letter must be upper case, or automatically changing it. Which approach you take depends on your organization’s culture. In some companies it is OK to have the system take decisions for you, while other organizations need to give the editorial team full control about any content that gets created. Let's build an event system to deal with the second approach (letting the editors know about the errors of their ways):



Step 1: create an Event Class Library with Visual Studio.


Add some references from [Tridion-Home]\bin\client


Then let’s write some code to validate if your first character is upper case, and notify the editor if that’s not the case.
using System;
using System.Collections.Generic;
using Tridion.ContentManager.ContentManagement;
using Tridion.ContentManager.ContentManagement.Fields;
using Tridion.ContentManager.Extensibility;
using Tridion.ContentManager.Extensibility.Events;

namespace ValidateTitleFieldUpperCaseFirstLetter
{
    [TcmExtension("ValidateTitleFieldUpperCaseFirstLetter")]
    public class Validate : TcmExtension, IDisposable
    {
        private readonly List<EventSubscription> _subscriptions = new List<Eventsubscription>();

        public Validate()
        {
            Subscribe();
        }

        private void Subscribe()
        {
            EventSubscription subscription =
                EventSystem.Subscribe<Component, SaveEventArgs>(ValidateFirstLetterIsUpperCase, EventPhases.Initiated);
            _subscriptions.Add(subscription);
        }

        private void ValidateFirstLetterIsUpperCase(Component component, SaveEventArgs args, EventPhases phases)
        {
            if (component.Schema.Title != "Article") return;
            if (component.ComponentType != ComponentType.Normal) return;
            ItemFields fields = new ItemFields(component.Content, component.Schema);
            SingleLineTextField titleField = (SingleLineTextField)fields["Title"];
            if(titleField.Values.Count == 0)
            {
                // Tridion should take care of this, but just in case someone changed the field to optional
                throw new Exception("Title field is mandatory.");
            }
            string fieldContent = titleField.Value;
            if(char.IsLower(fieldContent[0]))
            {
                throw new Exception("The first letter of the Title field must be a Capital letter.");
            }
        }

        public void Dispose()
        {
            foreach (EventSubscription subscription in _subscriptions)
            {
                subscription.Unsubscribe();
            }
        }
    }
}

Now let’s build our code and tell Tridion to execute it (add a line similar to this to Tridion.ContentManager.Config under "<extensions>"

<add assemblyFileName="D:\Tridion 2011\PathToYourDll\ValidateTitleFieldUpperCaseFirstLetter.dll" />

Restart Tridion (COM+ and Service Host) and let’s see what it does.


So, it’s a nice improvement from the previous message. Cost of development? Well, it took me about 20 minutes to write it and deploy it (fair enough, it’s far from ready, with things that you shouldn’t see in production like "if (component.Schema.Title != "Article") return;"), but you get the picture.

You could also just be slightly smart about it and outline this requirement in the field’s description:
But this is too specific to this use case, and it’s a simple one after all.


Next (within a few days, I promise) we will start looking at how we could do this with Anguilla.

Friday, September 23, 2011

Recursing through Group Members with the Core Service

Yesterday I had to write a simple method to get me a list of all users who are members of a given group, using the Core Service.

Obviously, this is one of those tasks you'll immediately classify as "simple" or "easy" or "low complexity". Which is probably true, if you happen to have done this before...

Only one added twist, the list should contain all users who are members of this group, including sub-groups (groups members of the same group).

In TOM.NET this is a trivial foreach(Trustee trustee in group.Members), but in CoreService-land (and WCF-land) we don't have the Object Model available, only the data model. So I had to twist my mind for a while to get this to work, but eventually got it working as follows.

public Dictionary<string, string> GetUserEmailsByName(GroupData group)
{
    Dictionary<string, string> result = new Dictionary<string, string>();
    GroupMembersFilterData groupMembersFilter = new GroupMembersFilterData();
    foreach (XmlNode groupMemberNode in _coreServiceClient.GetListXml(group.Id, groupMembersFilter))
    {
        TrusteeData trustee = (TrusteeData)_coreServiceClient.Read(groupMemberNode.Attributes["href", Constants.XlinkNamespace].Value, ReadOptions);
        if (trustee is UserData)
        {
            if (trustee.Description.Contains("@"))
            {
                string userDescription = trustee.Description;
                string userEmail = UserEmailRegex.Match(userDescription).ToString();
                userDescription = userDescription.Replace(userEmail, "").TrimEnd();
                userEmail = userEmail.Replace("(", "").Replace(")", "");
                if (!result.ContainsKey(userDescription))
                    result.Add(userDescription, userEmail);
            }
        }
        else if (trustee is GroupData)
        {
            Dictionary<string, string> subMembers = GetUserEmailsByName((GroupData)trustee);
            foreach (string key in subMembers.Keys)
            {
                if (!result.ContainsKey(key))
                    result.Add(key, subMembers[key]);
            }
        }
    }
    return result;
}

The hard part to figure out was how to get the list of "Members" for this group, since GroupData does not expose that information:

_coreServiceClient.GetListXml(group.Id, groupMembersFilter)

Adding one more to the bag of CoreService patterns...

Saturday, August 20, 2011

Using Dreamweaver Field Notation in Tridion C# code

If you've done any Tridion Dreamweaver Templates you have surely come to appreciate the DW notation syntax that Tridion introduced with version 5.3. Compared to what we had to do before to read a field, this syntax was much easier - both on the eye and on the sanity.

If you have no clue what I'm talking about, here's an example of the difference between old-school (pre-2008) template code to read one field from a component, and the new way:

Old School
[%= Component.Fields("summary").value(1)%]
Dreamweaver Notation
@@summary@@

Not very different you say? What about reading a value from an embedded field then?
Old School
[%=Component.Fields("paragraph").value(1).Fields("embeddedsummary").value(1)%]
Dreamweaver Notation
@@paragraph.embeddedsummary@@


OK, so you agree with me now? If you don't agree yet, then try comparing the code when looping through multivalue collections...


Another great feature of Compound Templating (perhaps the best feature some will say) was the introduction of Template Mediators. This allows us to (out of the box) use .NET-based languages as Template Building Blocks, both using .NET assemblies or c# Fragments. Some of you may even use the Community owned (mostly written and maintained by Yoav Niran) XSLT Mediator that lets you use XSLT Building Blocks in Compound Templates.

So, plenty of good stuff. One thing that grandly p****d me off with the c# implementation was that it was still as complex - if not more - to read a component's field value than it was before with VBScript.

Here's how you would read the same Embedded Summary field using c#:
Component c = (Component)engine.GetObject(package.GetByName(Package.ComponentName));
ItemFields fields = new ItemFields(c.Content, c.Schema);
ItemField embeddedField = (ItemFields)fields["paragraph"];;
TextField textField = embeddedField["embeddedsummary"];
String textFieldValue = textField.Value;

Not really user friendly, is it? Another thing you may notice is that the field Type is also needed when reading a field, so what happens if the field type changes from, say, SingleLineTextField to KeywordField? Yup, your code will not work anymore and needs to be changed (there's a lot to say about Schema changes, but that's for another day).

So, why can't we use a DW-like notation in c#? Well, the short answer is that you can't because Tridion didn't provide it for you. The longer answer is that if you're one of the near 1000 people that downloaded the Dreamweaver Get Extension from Tridion World, you can, and here's how.

  1. Reference Tridion.ContentManager.Extensions.Templating.dll from your Visual Studio Project
  2. Add "using Tridion.ContentManager.Extensions;" to your .cs
  3. Start using the "FieldOutputHandler" class
Here's an example to get you started:
FieldOutputHandler h = new FieldOutputHandler(page.Id, engine, package);
meta.Add("Title", h.GetStringValue("Metadata.SEO.SEOTitle"));
meta.Add("Keywords", h.GetStringValue("Metadata.SEO.SEOKeywords"));
meta.Add("Description", h.GetStringValue("Metadata.SEO.SEODescription"));

This class has a lot of parameters, configuration settings, SiteEdit-related instructions, and also can be used for a lot more than just outputting string values...

For one, it can read _any_ field as a String, so you don't need to worry about the field's type.
Second, it can - just like the Get Extension - read fields from other objects, like Keyword Metadata, publication Metadata, etc, etc.
Third, you can drill down into a linked component's field value, which could also be a linked component, etc, etc.

Here's another example where this handler is being used to loop through values of a configuration component:
for (int x = 0; x < TotalConfigs; x++)
{
    FieldOutputHandler h = new FieldOutputHandler(PubMeta.GetComponentLinkField("Metadata.Configuration").Values[x].Id, engine, package);

    int varCount = h.GetEmbeddedSchemaField("Fields.Keys").Values.Count;
    for (int i = 0; i < varCount ; i++)
    {
        String name = h.GetStringValue(String.Format("Fields.Keys[{0}].Key", i));
        String value = h.GetStringValue(String.Format("Fields.Keys[{0}].Value", i));
        if (package.GetByName(name) == null)
        {
            package.PushItem(name, package.CreateStringItem(ContentType.Text, value));
        }
    }
}

Last but not least, I know the class name sucks. I was quite uninspired that day. Have fun with your newly-discovered Tridion powers!

Sunday, June 05, 2011

Importing Content into Tridion

Something I get asked almost every time a new project starts - or a new consultant or partner starts working with Tridion - is how to import content into Tridion.

It kinda baffles me, since I always thought it is pretty easy to import content into Tridion, but apparently that's not the case. Here's a few things to consider, I'm pretty sure most of this applies to _any_ content management system, and is not really related to Tridion.

Well formed content
Though it seems obvious, I see many content migrations coming from systems that do not enforce a strict XML schema as Tridion does, and therefore a simple one-to-one migration will fail miserably. "Easy" workarounds on this one:
- Use XmlWriter when creating the content representation for Tridion, and the ItemFields collections to create your content in. If it fails validation, it will probably fail before you try to save it.
- When dealing with Rich Text fields, use Tidy.NET to ensure your content is valid Xhtml.

Consider if a content migration is really what you want
One of the main reasons to change WCM is that your current content format does not match the business requirements. Guess what happens if you migrate your content "as-is" into Tridion? Yup, the content format still does not match your business requirements. So why are you even contemplating migration? Sure, you can get some of the content in, but you really should think about what you're trying to achieve before spending weeks writing a content migration tool that will prove to be worthless in a very short time frame. Do not underestimate the power of manual content migration in some cases.

How easy is it to get the source content?
This obviously depends on a lot of aspects of your current/old WCM, not all of them are as easy, and all of them are different.

In other words, really think about what it is you're trying to achieve before embarking on a migration project that insists on changing mid-way through the migration.

Since you read this far, here's a couple of bonus code samples :)

Converting html to xhtml using Tidy.NET:
private const String XhtmlNamespace = "http://www.w3.org/1999/xhtml";
public static String ConvertHtmlToXhtml(String source)
{
    MemoryStream input = new MemoryStream(Encoding.UTF8.GetBytes(source));
    MemoryStream output = new MemoryStream();

    TidyMessageCollection tmc = new TidyMessageCollection();
    Tidy tidy = new Tidy();


    tidy.Options.DocType = DocType.Omit;
    tidy.Options.DropFontTags = true;
    tidy.Options.LogicalEmphasis = true;
    tidy.Options.Xhtml = true;
    tidy.Options.XmlOut = true;
    tidy.Options.MakeClean = true;
    tidy.Options.TidyMark = false;
    tidy.Options.NumEntities = true;

    tidy.Parse(input, output, tmc);

    XmlDocument x = new XmlDocument();
    XmlDocument xhtml = new XmlDocument();
    xhtml.LoadXml("<body />");
    XmlNode xhtmlBody = xhtml.SelectSingleNode("/body");

    x.LoadXml(Encoding.UTF8.GetString(output.ToArray()));
    XmlAttribute ns = x.CreateAttribute("xmlns");
    ns.Value = XhtmlNamespace;
    XmlNode body = x.SelectSingleNode("/html/body");
    if (body != null)
        foreach (XmlNode node in body.ChildNodes)
        {
            if (node.NodeType == XmlNodeType.Element)
                if (node.Attributes != null) 
                    node.Attributes.Append(ns);

            if (xhtmlBody != null) 
                xhtmlBody.AppendChild(xhtml.ImportNode
                    (node, true));
        }
    return xhtmlBody != null ? xhtmlBody.InnerXml : null;
}


Getting a new or existing component (for update vs creation, CoreService with a custom client library)
static Component GetNewOrExistingComponent
    (string componentName, Folder folder)
{
    Component returnObject = null;
    componentName = SecurityElement.Escape(componentName);
    XmlNamespaceManager nm = new XmlNamespaceManager
        (new NameTable());
    nm.AddNamespace(Constants.TcmPrefix,
        Constants.TcmNamespace);
    CoreServiceSession session =
        new CoreServiceSession(CoreServiceEndpoint);
    OrganizationalItemItemsFilter filter = 
        new OrganizationalItemItemsFilter(session)
            {ItemTypes = new[] {ItemType.Component}};

    string xpath = String.Format
        ("tcm:Item[@Title='{0}']", componentName);
    XmlElement listItems = folder.GetListItems(filter);
    if (listItems != null)
        if (listItems.SelectNodes(xpath, nm).Count > 0)
        {
            string componentId = listItems.SelectSingleNode
                (xpath, nm).Attributes["ID"].Value;
            returnObject = session.GetObject
                (new TcmUri(componentId)) as Component;
        }
        else
        {
            returnObject = new Component(session, folder.Id);
        }
    return returnObject;
}

Note: This sample uses a custom client Library I wrote on top of the CoreService, and this library is not available yet - and I'm not sure I will make it available at all due to how long it took me to write it, and the fact that it is a work-in-progress. Releasing it means supporting it, and I unfortunately don't have the time to support my plants at home, let alone a still-half-buggy library that someone may try to use in production systems. Anyway, the code just looks like TOM.NET, so you shouldn't have any trouble reverse-engineering what this code does.

Saturday, April 10, 2010

Outputting a keyword hierarchy in XML

With Tridion 2009 we saw the introduction of hierarchical keywords - aka Taxonomy, aka Intelligent Navigation - and with it a whole new world of possibilities for content classification.

However, in some cases, you really don't want all the intelligence around it and would much rather have just an "old style" xml hierarchy. If you tried this yourself, you probably figured out by now that the keyword hierarchy doesn't look so... hierarchical when viewed through the API. Not at all.

It looks rather flat.

I had to write a template recently with some information about every keyword in the hierarchy (including its metadata) for a site navigation, and keeping the keyword "parent/child'' relationships.

Here's how can you do it.

Category Navigation = engine.GetObject(NAVIGATION_CATEGORY) as Category;
using (MemoryStream ms = new MemoryStream())
{
    XmlTextWriter w = new XmlTextWriter(ms, new System.Text.UTF8Encoding(false));
    w.Indentation = 4;
    w.Formatting = Formatting.Indented;

    w.WriteStartDocument();
    w.WriteStartElement("Navigation");
    log.Debug("/Navigation created");

    Filter filter = new Filter();
    filter.Conditions.Add("Recursive", false);
    foreach (XmlNode RootChildren in Navigation.GetListKeywords(filter).SelectNodes("//*[@IsRoot='true']"))
    {
        Keyword RootKeyword = engine.GetObject(RootChildren.Attributes["ID"].Value) as Keyword;
        log.Debug("Generating XML for keyword " + RootKeyword.Title);
        w.WriteStartElement("Item");
        w.WriteAttributeString("ID", RootKeyword.Id);
        w.WriteAttributeString("Title", RootKeyword.Title);
        if (RootKeyword.Metadata != null)
        {
            WriteKeywordMeta(RootKeyword, w);
        }
        WriteChildrenXml(RootKeyword.Id.ToString(), w);
        w.WriteEndElement();
    }
    w.WriteEndElement();
    w.WriteEndDocument();
    w.Flush();
    w.Close();

    package.PushItem(Package.OutputName, package.CreateStringItem(ContentType.Xml, Encoding.UTF8.GetString(ms.ToArray())));
}

And the recursive "WriteChildrenXml" method would look more or less like this:

private void WriteChildrenXml(String RootKeywordId, XmlWriter writer)
{
    Keyword ParentKeyword = _engine.GetObject(RootKeywordId) as Keyword;
    Filter filter = new Filter();
    filter.Conditions.Add("Recursive", false);

    foreach (Keyword ChildKeyword in ParentKeyword.GetChildKeywords(filter))
    {
        writer.WriteStartElement("Item");
        writer.WriteAttributeString("ID", ChildKeyword.Id);
        writer.WriteAttributeString("Title", ChildKeyword.Title);
        if (ChildKeyword.Metadata != null)
        {
            WriteKeywordMeta(ChildKeyword, writer);
        }
        WriteChildrenXml(ChildKeyword.Id.ToString(), writer);
        writer.WriteEndElement();
    }
}
Nuno