in

Telligenti

Serving up fresh ideas every day, Telligent style

Ben Tiedt

  • Community Server 2008 Hack-a-thon

    One of the fun benefits of working for Telligent is the ability to participate in regular "hack-a-thon" contests.  These contests usually involve working with a new feature or product and result in great prizes!

    Last week, while the Community Server team was in Dallas, we had a hack-a-thon to make cool products using the new REST API in Community Server 2008 in less than 20 hours (the contest started at 3PM and ended at 10AM the next day).  There were a lot of great entries and everyone showed what they had made the next day. 

    My entry was the Print/Send to Media Gallery application which makes it easy to get documents of any type into a CS media gallery.

    See more about the latest hack-a-thon on the Telligent site...

  • What is a Good Online Video Service for Screencasts?

    I'd like to start posting more video-based demonstrations and walk-throughs of theming and customizing Community Server, but I don't really want to store the videos on my shared web host (and pay for storage and bandwidth).

    What is a good online video service for screencasts?  Blip.tvVimeoVeoh? Other?

    Please let me know in the comments.  Also, please let me know of any topics that you'd like me to cover.

  • Community Server 2008 - Groups Overview

    Community Server 2008 includes a new feature, Groups.  Groups are miniature community sites within the larger community site, similar to Google- or Yahoo-groups.  Each Group has its own content, membership, and permissions.

     

    What's in a Group?

    A group can contain a single blog, single media gallery, single forum, and can also contain "pages."  The blog, media gallery, and forum can be individual enabled by the group owner. 

    The blog, media gallery, and forum behave exactly the same as the non-grouped versions of these applications with the exception that they are completely managed outside of the Community Server Control Panel.  These applications, instead, expose their management and moderation options through the group's pages and navigation options.

     

    How is a Group Created?

    By default, only system administrators can create groups, however, group creation can be enabled for any registered user of the Community Server site via the Group Administration control panel.  When a user is in a role that is granted the permission to create a group, that user can click the "Create a Group" link in the sidebar of the "Groups" home page to immediately create a group.

    System and Group administrators can also create, edit, and delete groups via the Group Administration Control Panel.

     

    Public/Private and Membership Options

    Groups can be defined to be "Public (Open Membership)", "Public (Closed Membership)", or "Private". 

    Public (Open Membership)

    Public groups can be viewed by users who are not members of the group, however, non-members cannot post new content or reply to existing content.  When the membership is "Open", any user of the Community Server site can choose to become a member without being approved by the owner of the group.  Membership is granted immediately.

    Public (Closed Membership)

    Just as with "Public (Open Membership)" groups, "Public (Closed Membership)" groups can be viewed by users who are not members of the group.  When membership is set to "Closed", however, users must request membership and be approved by the owner of the group before becoming a member of the group.

    Private

    Private groups and their content cannot be seen by non-members.  New members can only be added by owners of the group who either add the new member manually, or invite the new member by their email address.

     

    Group Membership Types and Permissions

    Within a group, there are four types of users: non-members, members, managers, and owners.  Each membership type is associated to a specific set of permissions within the group.

    Non-Members

    In private groups, non-members cannot view or interact with any content.

    In public groups, non-members can view (but not interact) with the group's content.

    Members

    Members can view and reply to existing content (blog posts, media files, forum threads) and view pages.  If enabled (via the "Edit Group" form), members may also be able to start new forum discussions or post their own media files.

    Managers

    Managers can do everything that regular members can, with the addition of: posting new pages and editing/deleting their own pages, posting/editing/deleting blog posts, starting new forum discussions (including sticky posts, announcements, polls, and video), editing/deleting any forum posts, moderating forum posts, posting/editing/deleting media files, and moderating media files.

    Owners

    Owners can do everything that managers can, with the addition of: editing the group, editing the group's theme, adding/removing members, and editing any pages.

     

    Theming Groups

    Each group can be individually themed (similar to blogs).  The owner of each group can select a theme for the group and configure any options exposed by that theme via the "Edit Group Theme" link in the administrative sidebar of the group.

    Customizing Group Themes

    The group-related themes are located in the web/themes/hubs/ folder (groups are called hubs within the API and themes).  Within theme files, the same <CSMedia:, <CSForum:, and <CSBlog: controls used for media gallery, forum, and blog content outside of groups can be used within the group to reference the group's content from these application types.  Additionally, the <CSHub: control prefix is used to identify group/hub-specific data and functionality.

    For more specific information about group-related controls, see the Chameleon documentation.

     

    This was a quick overview of the features, permissions, and theming options of groups.  Please ask any questions in the comments.

  • Dynamic Navigation Add On for Community Server

    Common requests for new users of Community Server are "how do I edit the navigation bar?", "how do I add a second level of navigation items?", and "how do I add drop-down menus to the navigation items?"  In all of these cases, changes are required to a few config files, some additional javascript needs to be added, and, even then, the items are only editable by modifying the theme files.  To make these changes/scenarios easier, I've implemented a new add-on for Community Server 2007 and 2008: Dynamic Navigation Support.

    This add-on extends the dynamic theme configuration support included in CS2007 and CS2008 to add support for editing the navigation items through the control panel.  It supports the following scenarios:

    1. A single row of navigation items
    2. Multiple rows of navigation items
    3. One or more rows of navigation items and drop-down sub-navigation items

    Another related request is "how do I add new content pages to Communtiy Server?"  With this add-on installed, new site articles can be created and added to the navigation bar via this add-on -- all without any additional editing of theme files!

    If you've wanted to implement any of these scenarios, this add-on makes it very simple (after a few setup steps... instructions are included in the download).  Give it a shot and let me know what you think in the comments.

    Note that this is not an official Telligent product and is provided as-is.  Questions should be directed to me directly on this site.

  • Community Server 2008 - Centralized File System (CFS) Overview

    In Community Server 2007 and earlier versions, the many file-related features (post attachments, site files, blog files, avatars, role icons, etc) all implemented their own file management logic and supported (a) storing files in the database (b) storing files in the file system or (c) configurable options allowing file storage in the database and/or file system.  Adding new file-based features (in the core application or in add-ons) required implementing custom file management logic.  Database-based storage was the default for post attachments.  Write permissions needed to be granted in many locations within the CS application.  Storing files in external systems (such as Amazon S3) required modifications to the custom file management logic used for each file-based feature.  In general, there were some file management issues. 

    In Community Server 2008, the Centralized File System (or Centralized File Storage or CFS) was implemented to centralize file management (so there was one instance of file management logic used for all file-based features), support file storage providers (so that new file storage services, such as Amazon S3, could be used to store files), and to make it generally easier to add file-based features to Community Server (both in the core product as well as in add-ons).

    The CFS sits deep in the Community Server API, between the API for file-based features and the configured storage providers:

     

    image

     

    Interacting with individual file-based features has not changed (or changed very little) between CS2007 and CS2008.  The individual file-based features, however, use the CFS instead of their own custom file management logic.

    In general, users and developers will not directly interact with the CFS.  Only when new file-based functionality must be implemented is direct access to the CFS required.  For example, to add/update/delete post attachments and their content, code should not access the CFS, but should instead access the add/update/delete methods of the PostAttachments class (which themselves interact with the CFS file store for post attachments).  When implementing a new feature, such as video transcoding, it may be useful to have a new file store and interact with the CFS directly as a new file-based feature.

     

    CFS Terminology

    The CFS uses file store keys, paths, and filenames to store files:

    File Store Key

    The file store key is the name identified on the <fileStore /> node representing the file store in the communityserver.config file (defined later).  It represents a unique set of files that are managed independently of other file stores.

    File store keys must be between 3 and 255 characters and cannot contain any characters that are not valid in windows file system paths.

    Path

    Paths are similar to directories, but are technically used more like keys.  Paths can represent multiple values separated by periods (the period is used for defining sub-paths and is also programmatically accessible via the CentralizedFileSystemProvider.DirectorySeparator property).  For example, a common file system path may be "my\files\are\here" and the similar CFS path would be "my.files.are.here"

    There is a technical difference between directories and CFS paths, however.  When querying for files in a directory, files are generally accessed within a single folder, "my\files\are" for example.  In the CFS, because paths are treated as keys, files can also be accessed using partial paths, such as "my.files.a", which would match files in the "my.files.are" path as well as the "my.files.and" and "my.files.any" paths.

    Paths must be between 0 and 769 characters and cannot contain any characters that are not valid in windows file system paths.

    Filename

    Filenames are the names of the files stored in the CFS.  They must be between 1 and 255 characters in length and must not contain any characters that are invalid in window file system file names.

     

    File Stores in the CFS

    Each file-based feature can use one or more "file stores" in the CFS.  Each file store is defined within the communityserver.config file by a <fileStore /> node within the <CentralizedFileStorage /> node. 

    Each file store must define two attributes: name and type.  Additionally, the downloadValidatorType attribute can optionally be set.

    name

    The name attribute defines the name of the file store (also referred to as the file store key) -- this is how the code used to implement file-based features will identify the file store when it interacts with the CFS. 

    type

    The type attribute identifies the CFS file storage provider to use to store files for the file store. 

    CFS file storage providers are .net classes that inherit and implement the CommunityServer.Components.CentralizedFileStorageProvider base class (in the CommunityServer.Components.dll assembly).  Community Server 2008 ships with two CFS file storage providers: AmazonS3FileStorageProvider and FileSystemFileStorageProvider.

    Each file store can define its own file storage provider, with its own configuration.  For example, post attachments could be stored in Amazon S3 but user avatars could be stored in the local file system.  By default, CS2008 uses the file system file storage provider for all file stores and stores files in the "filestorage/" folder.

    downloadValidatorType

    Each file store can optionally identify a download validator.  A download validator is a .net class that implements the CommunityServer.Components.ICentralizedFileAccessValidator interface (in the CommunityServer.Components.dll assembly). 

    A download validator is used to implement logic that identifies, for the file store, whether a file should be allowed to be accessed or not.  In CS2008, only post attachments identify a download validator -- it is used to ensure that users have view and/or read permission to the attachment's associated post before allowing the attachment to be downloaded.

    Additional Attributes

    Each provider requires additional attributes to be defined on the <fileStore /> node.  These additional attributes are documented in the comments just above the <CentralizedFileStorage /> node in the communityserver.config file included with Community Server 2008.

     

    Creating New CFS File Stores

    Creating a new CFS file store is as simple as adding a new <fileStore /> node to the <CentralizedFileStorage /> node of the communityserver.config file with a new name.  When the <fileStore /> node exists, that file store can be used to add/delete/query files.

    The existing file stores are named according to the full name of the class in the Community Server API that interacts with the file store.  For example, post attachments are created/updated/deleted via the CommunityServer.Components.PostAttachments class in the CS API so the CFS file store used to store post attachment files is named "CommunityServer.Components.PostAttachments."  I would suggest continuing this naming convention when adding additional file stores to the CFS.

     

    Coding Against a CFS File Store

    When implementing new file-based features for Community Server, I strongly suggest using the CFS for file storage.  The CFS provides a simple API for adding, deleting and querying files and paths.

    The following methods are exposed by the CFS (and must be supported by all CFS providers):  GetFile, GetFiles, GetPaths, AddPath, AddUpdateFile, Delete

    To interact with these methods, code must first retrieve the file storage provider for a specific file store.  This is done using the file store key, as in:

    CentralizedFileSystemProvider myFileStore = CentralizedFileSystemProvider.Instance("FILE_STORE_KEY");

    Which would load the file storage provider for the "FILE_STORE_KEY" file store (note that there must be a corresponding <fileStore /> node in the communityserver.config file named "FILE_STORE_KEY", otherwise, the Instance method will return null).  The file management methods could then be called on the myFileStore object.  Any operations preformed using the myFileStore object would be performed within the context of the file store named "FILE_STORE_KEY".

    ICentralizedFile GetFile(string path, string fileName)

    Gets the file store's provider's implementation of the ICentralizedFile interface representing the file with the given path and file name.  If the file is not found, the GetFile method will return null.

    ICentralizedFile file is an abstract representation of a file and supports retrieving the ContentLength, FileName, Path, and FileStoreKey of the file and also supports retrieving a stream of the content of the file (via the OpenReadStream method) and getting a URL to download a file (via the GetDownloadUrl method).

    List<ICentralizedFile> GetFiles(PathSearchOption searchOption)

    List<ICentralizedFile> GetFiles(string path, PathSearchOption searchOption)

    Gets a list of files for the given path matching the defined search option (TopLevelPathOnly, AllPaths).  When using the TopLevelPathOnly search option, the file store works like a file system.  When using the AllPaths search option, the file store works more like a key-based system. 

    Note that paths are technically keys, not directories.  If I have a file in the path "my.files.are.here", the file can be retrieved from the path "my.files" or "my.fil" when using the AllPaths search option.

    List<string> GetPaths()

    List<string> GetPaths(string path)

    Gets a list of paths within the defined path.  Omitting the path will retrieve all top-level paths.  Paths are separated using periods (identified by the CentralizedFileSystemProvider.DirectorySeparator property).

    void AddPath(string path)

    Adds a path. 

    There is no concept of "parent" folder and "child" folder, instead, paths are treated as keys.  So I could add a path named "my.files.are.here" just as easily as incrementally adding "my" then "my.files" then "my.files.are" and finally "my.files.are.here".  The CFS will handle either case. 

    ICentralizedFile AddUpdateFile(string path, string fileName, Stream contentStream)

    Adds or updates a file to the file store.  If a file with the same path and fileName already exists, it is overwritten.  The path does not need to be manually created.

    void Delete()

    void Delete(string path)

    void Delete(string path, string fileName)

    Deletes a file or path.  If the path is omitted, all paths and files are deleted from the file store.

     

     

    Implementing New CFS File Storage Providers

    New CFS file storage providers can be implemented by inheriting from the CommunityServer.Components.CentralizedFileStorageProvider.  A few notes regarding implementing custom CFS storage providers:

    1. Are You Sure?
      New CFS file storage providers do not need to be implemented to store file in the CFS.  Only when you want to store files outside of the file system or Amazon S3 will a new file storage provider be required.
    2. Resources
      Because the creation of new CFS file storage providers should be rare, I'm not going to list all of the considerations in this article.  Instead, I would suggest reviewing the notes and comments in the CentralizedFileSystemProvider class in the CS2008 SDK and the two included implementations for specific information and implementation guidelines for each method.
    3. ICentralizedFile
      Each CentralizedFileSystemProvider must also implement the ICentralizedFile interface and support reading files as a stream (via the ICentralizedFile.OpenReadStream method) and downloading files via a URL (via the ICentralizedFile.GetDownloadUrl method).  All CFS functionality should be fully implemented to support all of the ways a file store can be used.

     

    General CFS Notes

    A few general notes about using the CFS in CS2008:

    1. Avoid Publishing the ICentralizedFile.GetDownloadUrl()
      In general, the CentralizedFileStorageProvider.GetGenericDownloadUrl(ICentralizedFile) method should be used when rendering download links to a file.  The URL generated by this method is generic and, when accessed, will determine the current file storage provider for the file and redirect to it.  This will allow the storage provider to be changed without having to update all existing links to files (since ICentralizedFile.GetDownloadUrl() would otherwise return a storage-provider-specific URL that would not be valid when the storage provider is changed).
    2. Avoid Calling ICentralizedFile.OpenReadStream()
      Whenever possible, render a URL and don't read the contents of the file.  In most cases, a URL can be rendered to a user instead of reading a file and writing it manually.  In some cases, however, this is not possible... such as when resizing an image.
    3. When a URL is really a CFS-stored File
      When processing URLs, the CentralizedFileStorageProvider class exposes a few static methods that can help to translate a URL into a CFS-stored ICentralizedFilebool IsCentralizedFile(string url) and ICentralizedFile GetCentralizedFileByUrl(string url).  These methods can be used to detect and retrieve CFS files by their URL.  Note that these methods only work with URLs generated by the CentralizedFileStorageProvider.GetGenericDownloadUrl method.

     

    Questions? Comments?  It was a long article... feel free to ask in the comments.

  • Community Server 2008 - Implementing Custom File Viewers

    File viewers are the unsung heroes that make Community Server 2008 media-friendly.  They implement the support for viewing and resizing images, viewing uploaded and remote video files, viewing video from online video services, and playing uploaded and remote audio clips.  Without file viewers, the media gallery would be the file gallery from CS2007.

    File viewers are supported when rendering post attachments or by using the "Insert Media" toolbar item in the content editor for blog/media/forum posts, content parts, announcements, profile comments, conversation messages, user biographies, group descriptions, user signatures, HTML-based dynamic configuration properties, and the "Generic Content" widget.

    New file viewers can be easily implemented to support new or additional file types or rendering methods.

     

    Implementing Custom File Viewers

    File viewers are .net classes that implement the CommunityServer.Components.IFileViewer interface.  This interface defines only two methods (with two variations each): Render and GetMediaType.  The Render method is used to render a provided file or URL and the GetMediaType is used to retrieve the type of content that will be generated when a file or URL is rendered.

    string Render(ICentralizedFile file, FileViewType viewType, int width, int height)

    string Render(Uri url, FileViewType viewType, int width, int height)

    The Render method is called to get the HTML markup used to render a file or URL configured to be viewed by this file viewer.  For local files, an ICentralizedFile-implementing object will be provided that represents a file in the Centralized File System.  For URLs, the Uri object representing the URL is provided.  For both local files and URLs, the type of view (Preview or View) and the requested width and height (in pixels) is provided. 

    PostMediaType GetMediaType(ICentralizedFile file, FileViewType viewType)

    PostMediaType GetMediaType(Uri url, FileViewType viewType)

    The GetMediaType method is used by Community Server to determine what type of media will be rendered for a given view of a file.  The returned value is from the PostMediaType enumeration (Empty, Image, Video, Audio, Poll).  This value is used to set the PostMedia property of posts based on the files referenced within the post and/or the post attachment and can also be used to perform conditional formatting via the post-attachment-related view condition controls in Chameleon.

    I'd suggest downloading the Community Server 2008 SDK and looking at the existing file viewers for examples of file viewer implementations.  The existing viewers all exist in the Components/UI folder and use file names ending in "FileViewer."

     

    Registering Custom File Viewers

    All file viewers are registered with Community Server in the <FileViewers> region of the communityserver.config file.  Each file viewer requires an <add /> node such as the following (which is used for the image file viewer):

    <add
      type="CommunityServer.Components.ImageFileViewer, CommunityServer.Components"
      extensions="gif;jpg;jpeg;bmp;png"
      urlPattern="^[^\?]*?\.(?:gif|jpg|jpeg|bmp|png)(?:\?.*)?$"
      />

    In this example, all three supported attributes to the <add /> node are shown: type, extensions, and urlPattern.

    type

    The type attribute identifies the class implementing the IFileViewer interface that should be registered.  In this case, its the CommunityServer.Components.ImageFileViewer class in the CommunityServer.Components assembly. 

    extensions

    The extensions attribute identifies the semi-colon-separated list of file extensions that are supported by this viewer.  Note that these extensions will only be matched to local files (those stored in the Centralized File System in Community Server 2008) and will not match to remote attachments or external URLs.

    urlPattern

    The urlPattern identifies a regular expression that will match URLs that the file viewer can support.  In this case, the ImageFileViewer identifies that it can render URLs that identify an image file extension.

     

    File Viewer Implementation Notes

    The following are notes and guidelines regarding implementing file viewers:

    1. Width and Height are Suggestions
      The width and height provided to the IFileViewer.Render method should be treated as suggestions and/or not-to-exceed values.  For example, rending an audio clip at 400 pixels in height will probably not make sense.  Instead, as long as the audio clip fits within the suggested height, it could render at 32 pixels in height... which is a more reasonable value.
    2. View and Preview Rendering
      In general, when the FileViewType.Preview is requested, a static and or quick-rendering version of the file should be displayed -- an image or icon, in general.  When the FileViewType.View is requested, the full and/or interactive version of the file can be rendered -- a video player, audio player, <object />, <embed />, etc.
    3. File vs URL Support
      File viewers do not need to support both File and URL-based files.  The YouTubeFileViewer, for example, only supports URLs from YouTube. 

      If a file viewer does not support local files, the extensions attribute on its <add /> node in the <FileViewers /> region of the communityserver.config file can be omitted and the Render and GetMediaType methods that accept ICentralizedFile objects can throw exceptions or return default values (since they should never be called). 

      If a file viewer does not support URLs, the urlPattern on its <add /> node in the <FileViewers /> region of the communityserver.config file can be omitted and the Render and GetMediaType methods that accept Uri objects can throw exceptions or return default values (since they should never be called).
    4. FileViewer Priority
      Within the <FileViewers /> region of the communityserver.config file, order does matter. 

      For duplicate extensions (where multiple file viewers handle the same extension), the last viewer registered to handle the extension will be used to handle all files with the extension.

      For overlapping urlPatterns (where a single URL may match the urlPattern of more than on file viewer), the first matching file viewer will be used to handle the URL.

      Note that it is possible to register the same file viewer multiple times (with different extensions or urlPatterns) in the <FileViewers /> list to implement the desired priority.
    5. Rendering JavaScript
      In general, the IFileViewer.Render method can render whatever markup it chooses.  When rendering JavaScript, however, I suggest following these guidelines:
      1. Render Scripts Inline 
        When including script resources, either inline or external, render the <script></script> tag within the string returned by the Render method instead of attempting to register the script via the Page object or CSControlUtility class.  This will ensure that media embedding and the out-of-the-box slide shows will function properly.
      2. Do Not Use document.write
        In general, document.write should not be used in JavaScript.  To support media embedding, the out-of-the-box slide shows, and XHTML in general, document.write should not be used by file viewers.

     

    Questions?  Issues?  Let me know in the comments.

  • Improved Chameleon Documentation for Community Server 2008

    The updates to the official documentation sites are still a little way out, so, in the meantime, I've gotten permission to post the new, improved Chameleon control documentation on this site.

    Here it is: CS2008 Chameleon Documentation

    The new Chameleon documentation merges the old CHM download file and property documentation (along with some additional information and assumptions) to provide a single, integrated, searchable, control-based reference for all Chameleon controls in CS2008.  It also provides direct access to QueryOverrides information for list controls (which was previously difficult to find) and helpful "see also" links.

    Let me know what you think.  Questions? Issues? Suggestions?

  • Community Server 2008 - How to Implement Custom Widgets

    After adding support for widgets in custom themes, it may be useful to create a few custom widgets.

    Widgets in Community Server 2008 (internally identified as content fragments) are server-side .net classes implementing the CommunityServer.Components.IContentFragment interface.  Community Server will load all classes implementing this interface from all assemblies in the bin/ folder and make them available in the widget selection form (subject to application-type filters) -- there is no additional configuration required to add support for new content fragment types.

    To help implement new content fragments, CS2008 includes two base classes:  CommunityServer.Components.ContentFragmentBase and CommunityServer.Components.ConfigurableContentFragmentBase.  These base classes implement the administrative members of the IContentFragment interface and require that only the content-related members be implemented to complete a new content fragment.  Furthermore, the ConfigurableContentFragmentBase base-class adds support for exposing configuration options (via Dynamic Configuration) for new content fragments.

     

    Implementing a Basic Widget

    A basic widget is very easy to implement.  Simply create a new class library project in Visual Studio and add a reference to CommunityServer.Components.dll and System.Web.dll.  Then, create a new class file and inherit from CommunityServer.Components.ContentFragmentBase.  Allowing Visual Studio to complete the implementation of this abstract class automatically results in the following:

    using System;
    using System.Collections.Generic;
    using System.Text;

    namespace WidgetSamples
    {
        public class MyWidget : CommunityServer.Components.ContentFragmentBase
        {
            public override void AddContentControls(System.Web.UI.Control control)
            {
                throw new Exception("The method or operation is not implemented.");
            }

            public override string FragmentDescription
            {
                get { throw new Exception("The method or operation is not implemented."); }
            }

            public override string FragmentName
            {
                get { throw new Exception("The method or operation is not implemented."); }
            }
        }
    }

    To complete the implementation of this widget, the three required members above must be implemented. 

    void AddContentControls(Control)

    The AddContentControls method is used to populate the content of the widget.  All content must be added to the control.Controls collection.  To add literal text, the System.Web.UI.LiteralControl can be used, for example:

    public override void AddContentControls(System.Web.UI.Control control)
    {
        control.Controls.Add(new System.Web.UI.LiteralControl("This is my widget!"));
    }

    Because content fragments use Control objects, the content of widgets can take advantage of all of the existing ASP.Net and Chameleon controls.  Controls in widgets participate in the full page lifecycle and can initiate and handle call backs and post backs.

    string FragmentDescription

    The FragmentDescription returns the description of this content fragment and is rendered in the widget selection control used in theme configuration pages.  The description should be a single, concise sentence, such as:

    public override string FragmentDescription
    {
        get { return "Renders 'This is my widget!'"; }
    }

    string FragmentName

    The FragmentName returns the name of this content fragment and is rendered in the widget selection control as well, above the description.  The name should be only a few words, such as:

    public override string FragmentName
    {
        get { return "My widget"; }
    }

    Once these three members are implemented, the class library can be compiled and it's output DLL can be deployed to Community Server's bin/ folder.  Then, navigating to a theme configuration page,

     

    image image image

     

    Implementing a Configurable Widget

    Many widgets benefit from exposing end-user configurable options.  To support such options, the alternate CommunityServer.Components.ConfigurableContentFragmentBase base-class can be used.  The ConfigurableContentFragmentBase class is similar to the ContentFragmentBase class used in the simple widget example above, but adds support for exposing and using Dynamic Configuration options.

    To create a configurable content fragment, create a new class library project in Visual Studio and add a reference to CommunityServer.Components.dll, Telligent.DynamicConfiguration.dll, and System.Web.dll.  Then, create a new class file and inherit from CommunityServer.Components.ConfigurableContentFragmentBase.  Allowing Visual Studio to complete the implementation of this abstract class automatically results in the following:

    using System;
    using System.Collections.Generic;
    using System.Text;

    namespace WidgetSamples
    {
        public class MyConfigurableWidget : CommunityServer.Components.ConfigurableContentFragmentBase
        {
            public override void AddContentControls(System.Web.UI.Control control)
            {
                throw new Exception("The method or operation is not implemented.");
            }

            public override string FragmentDescription
            {
                get { throw new Exception("The method or operation is not implemented."); }
            }

            public override string FragmentName
            {
                get { throw new Exception("The method or operation is not implemented."); }
            }

            public override Telligent.DynamicConfiguration.Components.PropertyGroup[] GetPropertyGroups()
            {
                throw new Exception("The method or operation is not implemented.");
            }
        }
    }

    As you can see, the members are the same as with the simple widget with the exception of the new GetPropertyGroups method.

    PropertyGroup[] GetPropertyGroups()

    The GetPropertyGroups method is used to identify the Dynamic Configuration property groups, sub-groups, properties, selectable values, etc supported by the content fragment.  The layout of exposed properties is similar to themes' theme.config files, but instead of using an XML file, the layout is defined programmatically using classes in the Telligent.DynamicConfiguration.Components namespace.  For example, to define a simple text entry field (note that I added a "using Telligent.DynamicConfiguration.Components" statement to the top of the class file):

    public override Telligent.DynamicConfiguration.Components.PropertyGroup[] GetPropertyGroups()
    {
        PropertyGroup group = new PropertyGroup("group1", "Options", 0);
       
        Property textToRender = new Property("textToRender", "Text to Render", PropertyType.String, 0, "");
        group.Properties.Add(textToRender);

        return new PropertyGroup[] { group };
    }

    This defines a single configuration group (which renders as a tab) named "Options" containing a single string-based property named "Text to Render."  A full description of the Dynamic Configuration API is beyond the scope of this article, but the Intellisense for the Telligent.DynamicConfiguration.dll assembly as well as the existing examples of content fragments in the Community Server 2008 SDK should provide ample examples for defining configuration options.

    The rendering of the configuration form and storage of the values entered by end-users is handled automatically by Community Server based on the definition of the GetPropertyGroups method.

    Using Configuration Options

    To use the configuration options defined by the GetPropertyGroups method, the following methods (defined on the ConfigurableContentFragmentBase class) can be used based on the data type defined for the property:

    • bool GetBoolValue(string id, bool defaultValue)
    • int GetIntValue(string id, int defaultValue)
    • double GetDoubleValue(string id, double defaultValue)
    • string GetStringValue(string id, string defaultValue)
    • string GetHtmlValue(string id, string defaultValue)
    • DateTime GetDateValue(string id, DateTime defaultValue)
    • DateTime GetTimeValue(string id, DateTime defaultValue)
    • DateTime GetDateTimeValue(string id, DateTime defaultValue)
    • Guid GetGuidValue(string id, Guid defaultValue)
    • System.Drawing.Color GetColorValue(string id, System.Drawing.Color defaultValue)
    • System.Web.UI.WebControls.Unit GetUnitValue(string id, System.Web.UI.WebControls.Unit defaultValue)
    • Uri GetUrlValue(string id, Uri defaultValue)

    In all of these examples, the first parameter is the ID of the property for which a value should be retrieved.  In our sample definition of the GetPropertyGroups method above, we defined a single string field with the ID "textToRender."  To render that field as the content of the content fragment, the GetStringValue method can be used, for example:

    public override void AddContentControls(System.Web.UI.Control control)
    {
        control.Controls.Add(new System.Web.UI.LiteralControl(this.GetStringValue("textToRender", "")));
    }

    This will render the user-defined value for the "Text to Render" field as the content of the control.

    To finish the implementation of our configurable widget, the FragmentDescription and FragmentName still need to be implemented as with the simple widget:

    public override string FragmentDescription
    {
        get { return "Renders custom text"; }
    }

    public override string FragmentName
    {
        get { return "My Configurable Widget"; }
    }

    The class library can now be compiled and it's output DLL can be deployed to Community Server's bin/ folder.  Then, navigating to a theme configuration page,

     

    image image image image image

     

    Additional Options

    The base classes implement a few additional IContentFragment members that can be overridden by specific implementations to further customize widget rendering:

    string GetFragmentHeader(Control)

    By default, the FragmentName is rendered as the header of a rendered widget.  This behavior can be modified by overriding the base implementation of the GetFragmentHeader method.  This method returns the HTML to render as the content fragment's header. 

    The control parameter to this method can be used to load contextual data, for example, via the CommunityServer.Controls.CSControlUtility class.

    string GetFragmentMoreUrl(Control)

    The out-of-the-box themes in Community Server 2008 support rendering a "more URL" for each widget -- for example, viewing all tags for the tag cloud widget or viewing the full list of active posts for the active posts widget.  By default, the "more URL" is blank and will not be rendered.

    To set a "more URL", the GetFragmentMoreUrl method can be overridden to return the URL to navigate to view additional, related information.  Similar to the GetFragmentHeader method, the control parameter is supplied to support retrieving contextual information through Chameleon.

    ApplicationType[] GetRelatedApplicationTypes()

    As mentioned in the How to Support Widgets in a Custom Theme article, Community Server has built-in support for filtering available widgets by application types.  By default, the base classes identify no related application type which results in the content fragment being listed regardless of the application type filter. 

    To identify the application type or types that relate to a content fragment, the GetRelatedApplicationTypes method can be overridden to return the array of related application types.

     

    Any questions?  Please let me know in the comments.

  • Community Server 2008 - How to Support Widgets in a Custom Theme

    After updating a custom theme for CS2008, support for widgets can be added.

    Community Server 2008 provides built-in support for extending Dynamic Theme Configuration to support selecting custom widgets to be displayed within a theme.  CS2008 includes widgets for showing post lists, user lists, group lists, tag clouds, custom HTML (including viewer support for videos, images, and audio), as well as external widgets from Google, Widgetbox, and Springwidgets.  Here's a short example of using widgets in CS2008:

    (Please visit the site to view this media) 

    Adding support for widgets in a custom theme is simple--it requires only two steps:  (1) Add the Dynamic Configuration property for each "column" of widgets and (2) Add Chameleon controls to the theme to render the widget "column".

     

    Adding Widget Dynamic Configuration Properties

    Widget support in CS2008 is implemented as an extension to Dynamic Configuration support in themes.  For each set (or column) of widgets in a theme, a new <property /> node needs to be added to the theme's theme.config file.

    The following <property /> node identifies a column of widgets:

    <property
       id="myWidgetColumn"
       text="Widgets"
       dataType="Custom"
       controlType="CommunityServer.Controls.ContentFragmentsCustomControl, CommunityServer.Controls"
       />

    In this example, the id ("myWidgetColumn") is the unique name for this column of widgets (this will be used later when we want to render this column of widgets in the theme), the text identifies this widget column, the dataType identifies that widgets will be stored as custom data (they're actually stored as XML), and the controlType identifies the built-in Dynamic Configuration control that renders the widget editor:

    Example of the Widget Dynamic Configuration Control

    This <property /> node can be added within any existing <propertyGroup /> or <propertySubGroup /> node in the theme's theme.config file.  A full discussion of the theme.config file is beyond the scope of this article, but browsing around an existing theme.config file should provide enough pointers to add a widget column.

    A <property /> node identifying the dataType and controlType mentioned above must be added for each unique list/column of widgets that will be included in the theme.

    Once the <property /> nodes are added to the theme.config file, users are able to select their desired widgets.  The next step is actually rendering them somewhere in the theme...

     

    Adding Chameleon Controls to Render Widget Dynamic Configuration Properties

    Community Server 2008 provides Chameleon controls to render widgets.  Widgets are identified as ContentFragments in Chameleon and the following Chameleon controls can be used to interact with ContentFragments:

    • <CSControl:ContentFragment />
      Renders the content/body of a ContentFragment.
    • <CSControl:ContentFragmentData />
      API-related single value control that renders a property on a ContentFragment, such as: OrderNumber, FragmentName, FragmentDescription, FragmentHeader, or FragmentMoreUrl.
    • <CSControl:ContentFragmentList />
      List control that renders the list/column of widgets for a single configuration property (as defined in the theme's theme.config file).
    • <CSControl:ContentFragmentPropertyComparison />
      Condition control providing support for comparing two properties of a ContentFragment.
    • <CSControl:ContentFragmentPropertyValueComparison />
      Condition control providing support for comparing a property of a ContentFragment to a static value.

    These controls can be included in any page, master page, or user control in a theme.

    The most simple inclusion would be the default <CSControl:ContentFragmentList /> control:

    <CSControl:ContentFragmentList Property="myWidgetColumn" runat="server" />

    This would render the body/content of each ContentFragment configured for the property with id "myWidgetColumn" within <li> tags in an <ul> list.

    As with other Chameleon controls, the formatting of ContentFragments can be customized by defining custom formatting templates on the list control.  The following is the standard format for ContentFragment lists rendered in the Hawaii theme:

    <CSControl:ContentFragmentList runat="server" Property="myWidgetColumn">
      <ItemTemplate>
        <CSControl:ContentFragment runat="server">
          <LeaderTemplate>
            <div class="CommonContentBox">
              <CSControl:ContentFragmentData Property="FragmentHeader" runat="server" Tag="H4" CssClass="CommonContentBoxHeader" />
              <div class="CommonContentBoxContent">
          </LeaderTemplate>
          <TrailerTemplate>
              </div>
            <CSControl:ContentFragmentData ResourceName="ViewMore" LinkTo="More" runat="server" Tag="Div" CssClass="CommonContentBoxFooter" />
            </div>
          </TrailerTemplate>
        </CSControl:ContentFragment>
      </ItemTemplate>
    </CSControl:ContentFragmentList>

    Note the custom formatting of the <ItemTemplate /> of the <CSControl:ContentFragmentList /> and the customizations of the rendering of the <CSControl:ContentFragment /> to define a custom <LeaderTemplate /> and <TrailerTemplate />.  In this example, the <LeaderTemplate /> is used to render the ContentFragment's header and the <TrailerTemplate /> is used to render the ContentFragment's "view more" link if it is defined.

    The control templates can be adjusted to fit the look/feel/markup of any custom theme.

     

    Additional Options

    There are a few additional notes and considerations when adding support for widgets to a theme: Setting Default and Filtering Selectable Widgets.

    Setting Defaults for Widget Dynamic Configuration Properties

    When shipping a custom theme, it is useful to include a default set of widgets for each column of widgets supported by the theme.  The easiest way to add default widgets is to

    1. Define the widget <property /> node and add the supporting Chameleon controls to the theme as described above.
    2. Use the theme configuration page for the theme to select and configure the widgets that you want to include by default and save the theme changes.
    3. When your widgets are properly defined, use the export feature of the theme configuration page to get the sharable XML version of your configuration data.
    4. Locate the <property /> node in this XML file with the id matching the id given to each widget column.
    5. For each <property /> node relating to a widget column in the XML export file, copy the contents of the <value /> node (<![CDATA[... blah ... ]]>) and paste it into a <defaultValue /> node within the <property /> node defining the widget column in the theme's theme.config file.

    The final result should look like:

    <property id="myWidgetColumn" text="Widgets" dataType="Custom" controlType="CommunityServer.Controls.ContentFragmentsCustomControl, CommunityServer.Controls">
      <defaultValue>
        <![CDATA[
          <contentFragments>
            <contentFragment type="CommunityServer.Controls.SiteUrlContentFragment, CommunityServer.Controls" id="2df9ece8-fe74-46c6-b82a-a42a65664fe8" configuration="fragmentTitle=Shortcuts&amp;controlpanel=False&amp;blogAdmin=False&amp;mediaAdmin=True&amp;user_List=False&amp;post_NotRead=False&amp;forumSubscriptions=False&amp;usersOnline=False&amp;forumModeration=False&amp;hub_create=False&amp;allGroups=False&amp;groupAdmin=False&amp;customLinks=&amp;listCssClass=CommonSidebarList" />
            <contentFragment type="CommunityServer.Controls.TagCloudContentFragment, CommunityServer.Controls" id="2b1a7190-3b89-4a19-985c-eaf6c41e993d" configuration="applicationType=MediaGallery&amp;ignoreFilterTags=False&amp;maximumTags=25&amp;minimumPostsPerTag=0&amp;filterTags=&amp;fragmentTitle=Tags&amp;tagCloudCss=CommonSidebarTagCloud&amp;tagCss=CommonTag6%2cCommonTag5%2cCommonTag4%2cCommonTag3%2cCommonTag2%2cCommonTag1&amp;showTagCounts=False&amp;noTagsText=No+Tags+Found&amp;linkTo=ContextualTagBrowser" />
          </contentFragments>
        ]]>
      </defaultValue>
    </property>

    This will set the default set of widgets used by a new installation of the theme as well as the set of widgets used after clicking the "Restore Defaults" button on the theme configuration page.

    Filtering the Available Widgets for a Widget Dynamic Configuration Property

    If a column of widgets is only going to be shown in a blog, why show forums-related widgets in the widget drop-down?

    CS2008 supports filtering the widget drop-down list by application type.  To add an application-type-based filter to a widget dynamic configuration property, add the applicationTypes attribute to the <property /> in the theme.config file defining the widget column.  The value of this attribute is a comma-separated list of the application types for which associated widgets should be listed. For example,

    <property
       id="myWidgetColumn"
       text="Widgets"
       dataType="Custom"
       controlType="CommunityServer.Controls.ContentFragmentsCustomControl, CommunityServer.Controls"
       applicationTypes="Forum,Weblog"
       />

    would only allow blog- and forum-related widgets to be selected for this widget list/column.

    Note that when filtering widgets by application type, widgets that do not identify an associated application type will always be included in the list of available widgets--this includes the "Generic Content", "Google Gadget" and other application-generic widgets.

     

    In my next article covering CS2008, I'll talk about implementing custom widgets.

    Questions? Please ask in the comments.

  • Community Server 2008 - How to Upgrade Themes from CS2007 to CS2008

    My last article outlined the changes made to Chameleon and Theming between CS2007 and CS2008 and could be used as a guide for an in-depth, manual upgrade of a CS2007 theme to CS2008.  In this article, however, I'll provide my suggested approach to upgrading themes for CS2008.

     

    Upgrading a CS2007 Site Theme for CS2008

    1. Remove Old Files/Folders
      The following files and folders are no longer used in CS2008 and should be removed (if they exist) to prevent unnecessary review when upgrading to future versions of Community Server:
      1. Files/ folder
      2. Galleries/ folder
      3. Roller/ folder
      4. User/privatemessagelist.aspx file
    2. Search and Replace
      Some changes from CS2007 just require a few simple search-and-replaces against a theme's files.  Using a tool that can perform search-and-replace operations within files in a folder (including sub-folders), such as my personal favorite EmEditor, perform the following case-insensitive replacements across all files in the custom theme:
      1. LinkTo="UserPrivateMessages" to LinkTo="UserConversations"
      2. LinkTo="PrivateMessage" to LinkTo="SendConversationMessage"
      3. ImagesBaseUrl="~/Themes/default/images/common/" to ImagesBaseUrl="~/utility/images/"
      4. Src="~/Themes/default/common/UserWelcome.ascx" to Src="~/utility/usercontrols/UserWelcome.ascx"
      5. Formatter.Truncate( to Formatter.MaxLength(
      6. Formatter.CheckStringLength( to Formatter.MaxLength(
      7. Formatter.StripAllTags( to Formatter.RemoveHtml(
    3. Search and Think
      All <CSFile:> and <CSGallery:> prefixed controls are no longer valid in CS2008.  I'd suggest searching for such controls and either (1) remove them or (2) replace them with an appropriate <CSMedia:> prefixed control.  To find all remaining references to these controls, use a search tool (as used in step 2) to find:
      1. <CSFile:
      2. <CSGallery:
      Additionally, occurrences of the following inline code should be updated as these methods are no longer supported.  For each occurrence, I would suggest reviewing the corresponding theme file from one of the CS2008 themes and implementing the modification that was made in those themes in the custom theme.  Occurrences of the following text should be reviewed:
      1. Formatter.FormatAgoDate
      2. Formatter.WhiteSpace
      3. Formatter.FormatUsername
    4. Copy New Files/Folders
      With new functionality comes new theme files.  The following files did not exist in CS2007 but are required for a full-featured CS2008 theme.  The following files can be copied from an existing CS2008 theme (Hawaii, Calypso, and Lean-and-Green support all CS2008 features) and then modified to work with the custom theme's master pages and styles.  The following files and folders are new to CS2008:
      1. User/activitylist.aspx file
      2. User/announcementlist.aspx file
      3. User/commentlist.aspx file
      4. User/conversation.aspx file
      5. User/conversationlist.aspx file
      6. User/createeditconversation.aspx file
      7. User/createeditprofilemessage.aspx file
      8. User/editapikeymodal.aspx file
      9. User/editapikeys.aspx file
      10. User/favoritelist.aspx file
      11. User/friendshiplist.aspx file
      12. User/requestfriend.aspx file
      13. User/userfile.aspx file
      14. User/userfilelist.aspx file
      15. User/userfileslideshow.aspx file
      16. Utility/insertmedia.aspx file
      17. Media/ folder
      18. Hubs/ folder
    5. Copy Heavily Modified Files
      Some theme files in CS2008 were modified heavily to support new features.  For these files, I would suggest copying the CS2008 version of the file from one of the included themes (Hawaii, Calypso, or Lean-and-Green) and restyle the copied file to work with the custom theme.  The following files were heavily modified:
      1. Forums/createeditforumpostform.aspx
      2. Forums/thread-threadedview.ascx
      3. User/edituser.aspx
      4. User/userprofile.aspx
      5. Utility/selectcontent.aspx
    6. Be Aware of CSS Rules
      When copying files from an existing theme to a custom theme, be sure to review the CSS classes and rules used by those files.  While copying the theme pages will render the content, without the associated CSS style rules, the pages may not render well.

    NOTE: There were many changes to the underlying API in Community Server.  While these steps should cover most themes, if a CS2007 theme relied heavily on inline code, it is possible that some of the API references are no longer valid.  In themes that make frequent use of inline code, I suggest manually reviewing all of the inline code for the theme to ensure that it is valid in CS2008.

     

    Upgrading a CS2007 Blog Theme for CS2008

    Luckily, fewer changes were made to blog themes in CS2008.  To upgrade a blog theme, follow steps #2 and #3 above.

     

    Group/Hub Themes in CS2008

    Sub-communites, referred to as "Groups" in the UI and "Hubs" in themes and the API, are new to Community Server 2008.  To support groups/hubs with a custom theme upgraded from CS2008, a new theme within the themes/hubs/ folder will need to be created.

    As with new blog and site themes, I suggest copying an existing theme and implementing the desired changes.

     

    Questions?  Issues?  Let me know in the comments.

  • Community Server 2008 - Changes to Chameleon

    In this post, I'll outline the changes to the Chameleon theming engine (and themes in general) between Community Server 2007 and Community Server 2008.  If you're interested in the changes to themes between an earlier version of Community Server (before 2007) and the 2008 version, I suggest reading my Introduction to Chameleon posts from the release of CS2007.

     

    New Functionality

    Community Server 2008 merges the File Gallery and Photo Gallery applications into a single Media Gallery application.  New messaging support (activities, profile comments, profile announcements, and private conversations) was also added.  For all of the new API objects, there are new Chameleon controls.  Also, for all of the new UI locations, Community Server expects new pages to exist in themes.

    In terms of themes and Chameleon, this means:

    1. All controls with the <CSFile:> and <CSGallery:> prefix from CS2007 no longer exist in CS2008 -- they cannot be used anywhere in a CS2008 theme.
    2. Pages linking to or displaying private messages must now use the new <CSMessage:> controls.
    3. The "PrivateMessage" and "UserPrivateMessages" values for the LinkTo property of the <CSControl:UserData /> control should be changed to "SendConversationMessage" and "UserConversations" (respectively).
    4. All theme pages in the Files/, Galleries/, and Roller/ folders of a theme are no longer used.
    5. The User/privatemessagelist.aspx theme page is no longer used.
    6. The following pages were added:
      1. User/activitylist.aspx
        Lists activity messages for a user
      2. User/announcementlist.aspx
        Lists announcements made by a user
      3. User/commentlist.aspx
        Lists comments made on a user
      4. User/conversation.aspx
        Displays a single conversation
      5. User/conversationlist.aspx
        Lists all conversations for the accessing user
      6. User/createeditconversation.aspx
        Starts and enables replying to a conversation
      7. User/createeditprofilemessage.aspx
        Adds a comment on a user or Adds and supports editing an announcement
      8. User/editapikeymodal.aspx
        Edits a REST API key
      9. User/editapikeys.aspx
        Lists and manages REST API keys
      10. User/favoritelist.aspx
        Lists a users favorite posts and sections
      11. User/friendshiplist.aspx
        Lists all friends for a current user and provides support for accepting and reviewing friendship requests
      12. User/requestfriend.aspx
        Initiates a friendship request
      13. User/userfile.aspx
        Displays a single file uploaded into a user's "My Files" area
      14. User/userfilelist.aspx
        Lists files uploaded into a user's "My Files" area
      15. User/userfileslideshow.aspx
        Displays a slideshow of files in a user's "My Files" area
      16. Utility/insertmedia.aspx
        Renders the "Insert Media" window used by the editor to insert file viewers for external or local media
      17. Media/*
        All files in the Media/ folder are new and are used to render the Media Gallery application pages
      18. Hubs/*
        All files in the Hubs/ folder are new and are used to render aggregate group information (Groups are called Hubs in the API, Chameleon, and themes)

     

    Removal of Base Page Types

    In CS2007, Chameleon required that all ASPX pages inherit from CSThemePage (or one of its sub-classes... CSBlogThemPage, CSFileThemePage, etc).  This is no longer the case in CS2008.

    To better support integrating content and functionality from Community Server in other ASP.Net-based web applications, Chameleon no longer requires that each ASPX page inherit from CSThemePage.  Instead, Chameleon now uses an application-type-specific "page context" object to interpret the querystring, register page headers, etc.

    For existing CS2007 themes, no changes are necessarily required.  The base theme pages still exist in CS2008 and expose the same functionality (such as the SetTitle() method and CurrentX properties), but, these methods/properties now use the base page context instead of implementing the functionality themselves.

    If a theme needs to use a different base page type, all references to the properties and methods of the CSThemPage must be removed (see New General-Purpose Chameleon Controls for more information).  The themes that ship with CS2008 should not be dependent on CSThemePage and can be used as a reference on a page-by-page basis.

     

    Changes to Existing Chameleon Controls

    With respect to the topic further down the page (Property Templates), there are very few changes to existing Chameleon controls and none of the implemented changes should impact existing themes.

    Here is an overview of the changes, by control type:

    API-related Single Value Controls

    The following properties were added to API-related Single Value Controls:

    • LinkTabIndex
      Defines the tab-index for the link identified by the LinkTo property.
    • EnsureVisibleHtml
      When set to true (the default is false), the content of the control will only be rendered if the value of the defined Property or ExtendedAttribute contains visible HTML -- that is, text or <img />, <object />, <embed />, <applet />, or <iframe /> tags.  This setting is helpful when determining whether to show content entered from an HTML editor.
    • IgnoreLinkPermissions
      When set to true (the default is false), the link identified by the LinkTo property will be rendered regardless of the accessing user's permissions to that link.  For example, in forums, the "Reply" link is displayed even for users who do not have access to reply to a post.  When the link is clicked, the user is either asked to log in or alerted that they do not have access to reply.
    • Encoding (None, HTML, JavaScript, URL)
      To help remove the need to use inline code, the Encoding property was added.  When set (the default is None), the value of the defined Property or ExtendedAttribute will be encoded for the identified output -- HTML, JavaScript or URL.  This property is useful when including dynamic content within inline JavaScript scripts or with property templates (defined below) that will be included in URLs.

    API-related Condition Controls

    API-related Condition Controls, such as XPropertyValueComparison and XPropertyComparison, now support an additional value for the Operator property: ContainsVisibleHtml.  Using this operator will cause the condition to evaluate to true only if the "left" value contains visible HTML (visible text or <img />, <object />, <embed />,