Sitefinity: Rss Feeds for Generic Content – Part 2

By in
No comments

 

As I mentioned in part one of this series, building an Rss feed for the Generic Content Module (or any module for that matter) requires three classes, RssChannelProvider, RssSettings, and RssView (plus the rss feed settings user control). We already covered the basic RssView, so now in this part we take a more detailed look at the changes required to the other two classes to implement the full Rss functionality.

RssSettings

The bulk of the rss setup is handled using this class, so most of our attention will be here. This is the class that facilitates the “setup” of your feed, including the feed title, description, and content to be syndicated. Since this is the link between the feed settings stored in the database and the settings user control, we use another class ControlContainer inside of the RssSettings class that handles references to all the input controls:

protected class ControlContainer : GenericContainer<GenericContentRssSettings>
{
    public ControlContainer(GenericContentRssSettings owner) : base(owner) { }

    private ListControl providerList;
    public ListControl ProviderList
    {
        get {
            if (providerList == null)
                providerList = base.FindRequiredControl<ListControl>("ProviderList");
            return providerList;
        }
    }

    private IEditableTextControl enclosureMetaField;
    public IEditableTextControl EnclosureMetaField
    {
        get {
            if (enclosureMetaField == null)
                enclosureMetaField = (IEditableTextControl)base.FindControl(typeof(IEditableTextControl), "EnclosureMetaField", true);

            return enclosureMetaField;
        }
    }

    private IEditableTextControl postUrl;
    public IEditableTextControl PostUrl
    {
        get {
            if (this.postUrl == null)
            {
                this.postUrl = (IEditableTextControl)base.FindControl(typeof(IEditableTextControl), "postUrl", true);
            }
            return this.postUrl;
        }
    }

    private IEditableTextControl summaryLength;
    public IEditableTextControl SummaryLength
    {
        get {
            if (this.summaryLength == null)
            {
                this.summaryLength = (IEditableTextControl)base.FindControl(typeof(IEditableTextControl), "summaryLength", true);
            }
            return this.summaryLength;
        }
    }

    private ListControl syndicationType;
    public ListControl SyndicationType
    {
        get {
            if (this.syndicationType == null)
            {
                this.syndicationType = base.FindRequiredControl<ListControl>("syndicationType");
            }
            return this.syndicationType;
        }
    }

    private IEditableTextControl itemsCount;
    public IEditableTextControl ItemsCount
    {
        get {
            if (this.itemsCount == null)
            {
                this.itemsCount = (IEditableTextControl)base.FindControl(typeof(IEditableTextControl), "itemsCount", true);
            }
            return this.itemsCount;
        }
    }

    private ListControl categoryList;
    public ListControl CategoryList
    {

        get {
            if (this.categoryList == null)
            {
                this.categoryList = base.FindRequiredControl<ListControl>("CategoryList");
            }
            return this.categoryList;
        }
    }
}

Note that this control uses the generic ListControl and IEditableTextControl instead of DropDownList and TextBox. This allows you to use more complex versions of these controls (such as Telerik RadControls) without breaking functionality. All this class does is return references to the individual input controls on the Rss Feed Settings User Control I mentioned last time. You might want to include error handling (such as if the control is not found, or if the control is of the wrong type).

Now that we have the link between settings and user input, we need to override the CreateChildControls method. This is the method that instantiates all the input user controls using the ControlContainer class and populates them with existing settings (if any) so that they can be saved to the database on creating (or update). I’ve done my best to comment the sequence of events below.

 

protected override void CreateChildControls()
{
    ctrlContainer = new ControlContainer(this);

    // instatniate the settings user control if (this.controlTemplate == null)
    {
        // make sure it exists if (File.Exists(this.Page.MapPath(this.ControlTemplatePath)))
        {
            this.controlTemplate = this.Page.LoadTemplate(this.ControlTemplatePath);
        }
        else {
            throw new NullReferenceException("Control Template does not exist");
        }
    }
    this.controlTemplate.InstantiateIn(this.ctrlContainer);

    // retrieve list of content providers string[] strArray = null;
    int count = ConfigHelper.Handler.Providers.Count;
    if (count > 1)
    {
        int num2 = 0;
        strArray = new string[count];
        foreach (ProviderSettings element in ConfigHelper.Handler.Providers)
        {
            // we only want the generic content providers if (element.Type.StartsWith("Telerik.Cms.Engine.Data.Providers.DefaultProvider"))
            {
                // make sure we have permission GlobalPermission permission = new GlobalPermission((GlobalPermissions)ContentManager.SecurityRoots[element.Name], CrudRights.View);
                if (permission.CheckDemand())
                {
                    // add to list of providers strArray[num2++] = element.Name;
                }
            }
        }
    }

    // add list of providers to dropdown list for selection ListControl providerList = this.ctrlContainer.ProviderList;
    if (strArray != null)
    {
        foreach (string str in strArray)
        {
            if (!string.IsNullOrEmpty(str))
            {
                ListItem item = new ListItem(str.Replace('_', ' '), str);
                providerList.Items.Add(item);
            }
        }
    }
    this.ctrlContainer.ProviderList.Visible = providerList.Items.Count > 1;
    this.ctrlContainer.ProviderList.AutoPostBack = true;

    // handle changes to selected provider to show its categories this.ctrlContainer.ProviderList.SelectedIndexChanged += new EventHandler(this.ProviderList_SelectedIndexChanged);

    // populate syndication type menu this.ctrlContainer.SyndicationType.Items.Add(new ListItem("Full Content", Enum.GetValues(typeof(SyndicationType)).GetValue(0).ToString()));
    this.ctrlContainer.SyndicationType.Items.Add(new ListItem("Summary", Enum.GetValues(typeof(SyndicationType)).GetValue(1).ToString()));
    this.ctrlContainer.SyndicationType.Items.Add(new ListItem("Title Only", Enum.GetValues(typeof(SyndicationType)).GetValue(2).ToString()));

    // grab settings from db and load into user control if ((this.settings != null) && (this.settings.Count > 5))
    {
        this.ctrlContainer.SyndicationType.SelectedValue = this.settings["SyndicationType"];
        this.ctrlContainer.ProviderList.SelectedValue = this.settings["ProviderName"];
        this.ctrlContainer.PostUrl.Text = this.settings["ItemUrl"];
        this.ctrlContainer.ItemsCount.Text = this.settings["ItemCount"];
        this.ctrlContainer.SummaryLength.Text = this.settings["SummaryLength"];
    }

    // set Provider and category settings["ProviderName"] = ctrlContainer.ProviderList.SelectedValue;
    settings["Category"] = ctrlContainer.CategoryList.SelectedValue;

    // load category menu and select current option PopulateCategories();
    if (this.ctrlContainer.CategoryList.Items.FindByValue(settings["Category"]) != null)
        this.ctrlContainer.CategoryList.SelectedValue = settings["Category"];

    // render control to the page this.Controls.Add(this.ctrlContainer);
}

You may have noticed that I added an event handler to the SelectedIndexChanged event in the ProviderList menu. This calls a method to retrieve the list of categories for that provider so that we can show its categories when we change to it. This method calls the same PopulateCategories() method from the initial setup.

protected void ProviderList_SelectedIndexChanged(object sender, EventArgs e)
{
    ListControl lst = sender as ListControl;
    settings["ProviderName"] = lst.SelectedValue;
    PopulateCategories();
}

private void PopulateCategories()
{
    this.ctrlContainer.CategoryList.Items.Clear();
    string provider = this.settings["ProviderName"];
    ContentManager mgr = new ContentManager(provider);
    IList cats = mgr.GetCategories();
    foreach (Telerik.Cms.Engine.Data.Category cat in cats)
    {
        this.ctrlContainer.CategoryList.Items.Add(cat.CategoryName);
    }
}

This allows us to syndicate only specified categories of content from the specific provider. I did this because I only want to publish a feed of my Dark Synthesis Mixes in my podcast, as opposed to all of my music. You will see how this works in the next section, however this can easily be modified to ignore the category meta field and show all content from the provider.

The last thing we need to do for this class is make sure that we have a method to save the settings to the database. This is done by overriding the SaveSettings() method.

public IDictionary<string, string> SaveSettings()
{
    settings = new Dictionary<string, string>();
    settings["ProviderName"] = this.ctrlContainer.ProviderList.SelectedValue;
    settings["ItemUrl"] = this.ctrlContainer.PostUrl.Text;
    settings["ItemCount"] = this.ctrlContainer.ItemsCount.Text;
    settings["Category"] = this.ctrlContainer.CategoryList.SelectedValue;
    settings["SyndicationType"] = this.ctrlContainer.SyndicationType.SelectedValue;
    settings["SummaryLength"] = this.ctrlContainer.SummaryLength.Text;
    return settings;
}

That’s pretty much it for this class. As you can see, editing settings for the feed is pretty straight forward. Just make sure you add the control to the Feed Settings User Control, as well as a proeprty to the ControlContainer class to reference it. Then make sure that your settings are saved to the database in the SaveSettings and loaded in the CreateChildControls method. Now lets move on to the class that actually creates the feed itself!

RssChannelProvider

This is the class that is responsible for building the feed itself. Luckily, the mechanics of actually writing the xml are abstracted away by an RssItem class, so all we have to do is create these items and pass them along to the class and it will handle the rest! The first thing you want to do is expand the Initialize method to extract the relevant settings from the internal settings property (this is the same dictionary of elements from the previous RssSettings class) and map them to local private variables for later use.

private string EnclosureMetaField;
private string Category;
private int ItemCount;
public override void Initialize(IDictionary<string, string> settings)
{
    base.Initialize(settings);
    Category = settings["Category"];
    ItemCount = Int32.Parse(settings["ItemCount"]);
}

Next we need to override the CreateDataSource method to retrieve the appropriate list of items using the settings  values we just retrieved.

protected override IList CreateDataSource()
{
    ContentManager mgr = new ContentManager(this.ProviderName);
    if (string.IsNullOrEmpty(Category))
        return mgr.GetContent("Publication_Date DESC");
    else {
        List<IMetaSearchInfo> filter = new List<IMetaSearchInfo>();
        filter.Add(new MetaSearchInfo(MetaValueTypes.ShortText, "Category", Category));
        return mgr.GetContent(0, ItemCount, "Publication_Date DESC", filter.ToArray());
    }
}

As you can see, this is just a matter of initializing the content manager with the correct ProviderName. the base Initialize already populates the internal ProviderName property with the correct provider name! Notice how we make use of the Category property to filter the results and only show the requested category. It is here that you can make changes to the list of items that are retrieved based on whatever logic you might require to suit the needs of your feed.

Now the DataSource creates an IList of IContent, which is the basic GenericContent class. We need to map this into the RssItem class so that it can be properly rendered. So the final method we need to override is GetRssItems, which creates the individual items for the feed.

public override IList<RssItem> GetRssItems(System.Collections.Specialized.NameValueCollection query)
{
    IList<RssItem> rssItems = base.GetRssItems(query);
    if (rssItems.Count == 0)
    {
        RssItem item = new RssItem();
        item.Title = "Empty Blog";
        rssItems.Add(item);
    }
    return rssItems;
}

As you can probably tell, most of the work is handled by the base method. However, if the feed contains no items, you are going to get an exception stating that the feed cannot be closed without writing an entry. Basically what this means is that you have an empty blog. I handle this exception by checking for an empty list and if that is the case, creating a blank dummy item to show information and prevent the error. You could also use this method to do any last minute changes to your feed items (such as changing the publish time, title, or anything else you might need to modify at the last moment before publication).

And that’s pretty much it! With just these two parts you have the basics for creating your own feeds from GenericContent, or any custom module in Sitefinity, once again proving the versatility and extensibility of this great CMS package (not to mention how much FUN it is!). We have just one more part to go, which is modifying the feed to handle enclosures (read: podcasts!). Stay tuned!

The following two tabs change content below.
Josh loves all things Microsoft and Windows, and develops solutions for Web, Desktop and Mobile using the .NET Framework, Azure, UWP and everything else in the Microsoft Stack. His other passion is music, and in his spare time Josh spins and produces electronic music under the name DJ SelArom. His other passion is music, and in his spare time Josh spins and produces electronic music under the name DJ SelArom.