From time to time, people ask how to programmatically create formats for emails for Notifications. I will describe how to do this for Notifications 2016 R2. This example won't work for Notifications 2012 even though some parts may appear similar. While this is possible using AFSDK's AFDeliveryFormat, the properties to use are not documented. Also the properties are quite complicated since they consist of serialized XAML documents with mysterious guids, numbers, and paths. I will describe how to create one from scratch. Note that this is only applicable for Email. WebService has its own format description. First we will need the following helper classes:

    public class EmailFormatBuilder
    {
        private FormatDocumentBuilder _subject, _body;

        public EmailFormatBuilder()
        {
            _subject = new FormatDocumentBuilder(true);
            _body = new FormatDocumentBuilder(false);
        }

        public EmailFormatBuilder(string subjectXaml, string bodyXaml)
        {
            _subject = new FormatDocumentBuilder(subjectXaml, true);
            _body = new FormatDocumentBuilder(bodyXaml, false);
        }

        public static EmailFormatBuilder Create()
        {
            return new EmailFormatBuilder();
        }

        public static EmailFormatBuilder CopyFrom(AFDeliveryFormat format)
        {
            string subject = format.Properties["Subject"];
            string body = format.Properties["Body"];
            return new EmailFormatBuilder(subject, body);
        }

        public EmailFormatBuilder Subject(Action<FormatDocumentBuilder> builder)
        {
            builder(_subject);
            return this;
        }

        public EmailFormatBuilder Body(Action<FormatDocumentBuilder> builder)
        {
            builder(_body);
            return this;
        }

        public void Build(AFDeliveryFormat format)
        {
            format.Properties["Subject"] = _subject.BuildDocumentString();
            format.Properties["Body"] = _body.BuildDocumentString();
        }
    }

    public class FormatDocumentBuilder
    {
        private static readonly XDocument DefaultDocument = XDocument.Parse(@"<FlowDocument osiann0:ANXamlFormatProcessor.ANXamlFormatVersion=""1"" xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"" xmlns:osiann=""clr-namespace:OSIsoft.AN.Notification;assembly=OSIsoft.PINotifications.Formatting.UI"" xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml"" xmlns:osiann0=""clr-namespace:OSIsoft.AN.Notification;assembly=OSIsoft.PINotifications.Formatting""><Paragraph></Paragraph></FlowDocument>");
        private static readonly XNamespace Presentation = XNamespace.Get("http://schemas.microsoft.com/winfx/2006/xaml/presentation");
        private static readonly XNamespace OsiannNamespace = "clr-namespace:OSIsoft.AN.Notification;assembly=OSIsoft.PINotifications.Formatting.UI";

        private readonly XDocument _initialDocument;
        private readonly List<FormatItem> _newItems = new List<FormatItem>();

        public FormatDocumentBuilder(bool singleLine)
        {
            _initialDocument = DefaultDocument;
            SingleLine = singleLine;
        }

        public FormatDocumentBuilder(string xamlDocument, bool singleLine)
        {
            _initialDocument = XDocument.Parse(xamlDocument);
            SingleLine = singleLine;
        }

        public bool SingleLine { get; set; }

        public static FormatDocumentBuilder Create(bool singleLine)
        {
            return new FormatDocumentBuilder(singleLine);
        }

        public FormatDocumentBuilder Text(string text)
        {
            var formatItem = new TextItem(text);
            _newItems.Add(formatItem);
            return this;
        }

        public FormatDocumentBuilder NewLine()
        {
            var formatItem = new NewLineItem();
            _newItems.Add(formatItem);
            return this;
        }

        public FormatDocumentBuilder AddEventFrameProperty(EventFrameProperties property)
        {
            return Content(Constants.EventFrameGuid, (int)property);
        }

        public FormatDocumentBuilder AddElementProperty(ElementProperties property)
        {
            return Content(Constants.TargetGuid, (int)property);
        }

        public FormatDocumentBuilder AddNotificationProperty(NotificationProperties property)
        {
            return Content(Constants.NotificationRuleGuid, (int)property);
        }

        public FormatDocumentBuilder AddElementAttribute(string relativePath, AttributeProperties property)
        {
            return Content(Constants.TargetElementAttributeGuid, (int)property, relativePath);
        }

        public FormatDocumentBuilder AddElementAttribute(AFAttributeTemplate attributeTemplate, AttributeProperties property)
        {
            string relativePath = attributeTemplate.GetPath(attributeTemplate.ElementTemplate);
            return AddElementAttribute(relativePath, property);
        }

        public FormatDocumentBuilder Content(Guid contentId, int property, string persistedContentName = null)
        {
            var formatItem = new ContentItem()
            {
                ContentId = contentId,
                Property = property,
                PersistedContentName = persistedContentName ?? string.Empty
            };

            _newItems.Add(formatItem);

            return this;
        }

        public string BuildDocumentString()
        {
            var doc = new XDocument(_initialDocument);
            XElement currentParagraph = doc.Descendants(Presentation + "Paragraph").LastOrDefault();
            if (currentParagraph == null)
            {
                currentParagraph = new XElement(Presentation + "Paragraph");
                doc.Root.Add(currentParagraph);
            }

            foreach (var item in _newItems)
            {
                if (item.UseNewParagraph)
                {
                    currentParagraph = new XElement(Presentation + "Paragraph");
                    doc.Root.Add(currentParagraph);
                }

                item.AppendContent(currentParagraph);
            }

            string result = doc.ToString();
            return result;
        }

        private abstract class FormatItem
        {
            public abstract bool UseNewParagraph { get; }

            public abstract void AppendContent(XElement element);
        }

        private class TextItem : FormatItem
        {
            public TextItem(string text)
            {
                Text = text;
            }

            public string Text { get; }

            public override bool UseNewParagraph => false;

            public override void AppendContent(XElement element)
            {
                element.Add(new XElement(Presentation + "Span", new XAttribute(XNamespace.Xml + "space", "preserve"), new XText(Text)));
            }
        }

        private class NewLineItem : FormatItem
        {
            public override bool UseNewParagraph => false;

            public override void AppendContent(XElement element)
            {
                element.Add(new XElement(Presentation + "LineBreak"));
            }
        }

        private class ContentItem : FormatItem
        {
            public Guid ContentId { get; set; }

            public int Property { get; set; }

            public string PersistedContentName { get; set; }

            public override bool UseNewParagraph => false;

            public override void AppendContent(XElement element)
            {
                var inlineContent = new XElement(OsiannNamespace + "ANInlineContent",
                    new XAttribute("ContentID", ContentId),
                    new XAttribute("ContentProperty", (int)Property),
                    new XAttribute("PersistedContentName", PersistedContentName ?? string.Empty)
                 );
                element.Add(inlineContent);
            }
        }
    }


    public class Constants
    {
        public static readonly Guid SystemGuid = new Guid(SystemId);
        public static readonly Guid DatabaseGuid = new Guid(DatabaseId);
        public static readonly Guid NotificationRuleGuid = new Guid(NotificationRuleId);
        public static readonly Guid TargetGuid = new Guid(TargetId);
        public static readonly Guid EventFrameGuid = new Guid(EventFrameId);
        public static readonly Guid EventFrameAttributeGuid = new Guid(EventFrameAttributeId);
        public static readonly Guid TargetElementAttributeGuid = new Guid(TargetElementAttributeId);
        public static readonly Guid TriggeringConditionGuid = new Guid(TriggerConditionId);
        public static readonly Guid EventDetailsHyperlinkGuid = new Guid(EventDetailsHyperlinkId);
        public static readonly Guid LinkGuid = new Guid(LinkId);


        internal const string SystemId = "{888F1705-C21E-49cb-8941-89ECC83EF8C1}";
        internal const string DatabaseId = "{A5407735-C1DA-4659-8B46-0C82A2450D30}";
        internal const string NotificationRuleId = "{418DF2DB-58ED-4c27-97DE-0A909BD8B2FE}";
        internal const string TargetId = "{67fad554-031e-4e5f-9798-7ebccb4e909e}";
        internal const string EventFrameId = "{9CFCDE7E-9975-4ACA-A6BA-47DC17119902}";
        internal const string EventFrameAttributeId = "{1E30FB46-1CB7-4FA6-8B11-5D017CF4E944}";
        internal const string TargetElementAttributeId = "{02BB2A93-34EC-4CFC-9624-C1FE5CAF4287}";
        internal const string TriggerConditionId = "{15628018-3792-4aad-b13f-ad324c406a84}";
        internal const string EventDetailsHyperlinkId = "{A2B13347-444A-44C1-9792-0E0EB56F336F}";
        internal const string LinkId = "{DB29FB18-97C3-4939-9F6C-C0282C64E779}";
    }


    public enum ObjectProperties
    {
        Name = 0,
        ID = 1,
        Description = 2,
    }


    public enum ElementProperties
    {
        Name = 0,
        ID = 1,
        Description = 2,
        Path = 1000,
    }


    public enum EventFrameProperties
    {
        Name = 0,
        ID = 1,
        Description = 2,
        Path = 1000,
        StartTime = 2001,
        EndTime = 2002,
        TriggerTime = 2003,
        Severity = 2004,
        TriggerName = 2005,
        TriggerExpression = 2006
    }


    public enum ElementTemplateProperties
    {
        Name = 0,
        ID = 1,
        Description = 2,
        Path = 1000,
    }


    public enum NotificationProperties
    {
        Name = 0,
        ID = 1,
        Description = 2,
        Path = 1000,
        EvaluationTime = 2003,
        EscalationLevel = 2006,
        IsFromTemplate = 4000,
    }


    public enum AttributeProperties
    {
        Name = 0,
        ID = 1,
        Description = 2,
        Path = 1000,
        ValueAtStartTime = 3000,
        UOM,
        TimeStampAtStartTime,
        IsBadValueAtStartTime,
        IsQuestionableValueAtStartTime,
        ValueAtEvaluationTime,
        TimeStampAtEvaluationTime,
        IsBadValueAtEvaluationTime,
        IsQuestionableValueAtEvaluationTime,
        File,
        FileName,
        FileAuthor,
        EmbeddedImage,
        DisplayName,
        Hyperlink,
        URL
    }

 

This has all those mysterious guids and numbers defined. It also parses the *Builder objects handle parsing and appending the XAML documents. Suffice to say a piece of content is defined by three items: a guid (content id), an integer (property id), and a string (persisted content name). The content id tells us what object it is associated with (Element, Notification, etc). The property ids tell us which property on the object. Some content items require a name or path (e.g. attribute values). These are stored in the persistedcontentname.

 

Note that this doesn't quite cover every scenario. Attachments, Links and Embedded Images are handled differently and will have to be left to another blog post.

 

Here is a short sample using this code to add some text and content after copying the default format:

 

        private static void DemoCreateFormat(PISystem system)
        {
            var db = system.Databases["formattest"] ?? system.Databases.Add("formattest");
            var elementTemplate = db.ElementTemplates.Add("Format Template Demo*");
            var nrt = elementTemplate.NotificationRuleTemplates.Add("NRT*");
            var att = elementTemplate.AttributeTemplates.Add("test1");
            
            var emailPlugIn = system.DeliveryChannelPlugIns["Email"];
            var globalDefaultFormat = system.DeliveryFormats[emailPlugIn.ID];
            var format = nrt.DeliveryFormats.Add("customformat", emailPlugIn);
            EmailFormatBuilder.CopyFrom(globalDefaultFormat).Subject(b => b.Text("some more text"))
                .Body(b => b.Text("additional text in body").Content(Constants.EventFrameGuid, 1).AddElementAttribute(att, AttributeProperties.ValueAtEvaluationTime))
                .Build(format);


            var myUser = AFContact.GetCurrentUser(system);
            if (myUser != null)
            {
                var ncts = AFNotificationContactTemplate.FindNotificationContactTemplatesByContact(myUser, AFSortField.ID, AFSortOrder.Ascending, 100);
                var defaultEmail = ncts.FirstOrDefault(nct => nct.DeliveryChannelPlugIn == emailPlugIn && nct.IsInternal);
                if (defaultEmail == null)
                {
                    // the default email endpoint has not been created just create it here.
                    defaultEmail = new AFNotificationContactTemplate(myUser, $"{myUser.Name} - Email");
                    defaultEmail.DeliveryChannelPlugIn = emailPlugIn;
                    defaultEmail.IsInternal = true;
                    defaultEmail.CheckIn();
                }


                var subs = nrt.Subscribers.Add(defaultEmail);
                subs.DeliveryFormat = format;
                Console.WriteLine($"Added subscription for {myUser.Name}");
            }
            else
            {
                Console.WriteLine("Current user was not found so subscription was not added");
            }
            
            db.CheckIn();
            Console.WriteLine($"Create format {format.Name} on {nrt.GetPath()}");
        }