Random Photo Widget for BlogEngine.net

Sunday, 25 October 2009 08:50 PM
by Coose

Again, I’ve seen hundreds of these out there, but they all seem to work differently from what I expected.  So…I decided to write my own.  It’s really not that sophisticated here.  What I decided to do is to display a random photo pulled from the HTML of a post that has one of the specified tags.

So, in the BlogEngine “widgets” directory, create a folder called “RandomPhoto”.

Create two .ascx UserControls called “edit.ascx” and “widget.ascx”.

[more: Dude, this is long one…click here to read the full post]

Widget.ascx

This is the control that will be displayed on the widget area.  It’s simple:

    1 <%@ Control Language="C#" AutoEventWireup="true" CodeFile="widget.ascx.cs" Inherits="widgets_RandomPhoto_widget" %>

    2 <asp:Panel ID="Panel1" runat="server" HorizontalAlign="Center">

    3     <asp:HyperLink runat="server" ID="PhotoLink" NavigateUrl="#" rel="lightbox">

    4         <asp:Image runat="server" ID="PhotoImage" ImageUrl="#" />

    5     </asp:HyperLink>

    6 </asp:Panel>

Yeah, that’s it.  It’s a panel, containing a link and an image.  We will have to set the NavigateUrl of the link in codebehind, as well as the ImageUrl of the Image.

    1 using System;

    2 using System.Collections.Generic;

    3 using System.Linq;

    4 using System.Web;

    5 using System.Web.UI;

    6 using System.Web.UI.WebControls;

    7 using System.Collections.Specialized;

    8 using System.IO;

    9 using System.Linq;

   10 using Funkymule.BlogEngine.Data;

   11 using System.Configuration;

   12 using System.Globalization;

   13 using System.Text;

   14 

   15 public partial class widgets_RandomPhoto_widget : WidgetBase

   16 {

   17     protected void Page_Load(object sender, EventArgs e)

   18     {

   19     }

   20 

   21     public override string Name

   22     {

   23         get { return "RandomPhoto"; }

   24     }

   25 

   26     public override bool IsEditable

   27     {

   28         get { return true; }

   29     }

   30 

   31     public override void LoadWidget()

   32     {

   33         StringDictionary settings = GetSettings();

   34 

   35         ChangeFrequency frequency = ChangeFrequency.Always;

   36         try

   37         {

   38             if (!string.IsNullOrEmpty(settings["ChangeFrequency"]))

   39                 frequency = (ChangeFrequency)Enum.Parse(typeof(ChangeFrequency), settings["ChangeFrequency"]);

   40         }

   41         catch (FormatException)

   42         {

   43         }

   44 

   45         int dx;

   46         int dy;

   47         int rr;

   48         int rl;

   49 

   50         int.TryParse(settings["dx"], out dx);

   51         int.TryParse(settings["dy"], out dy);

   52         int.TryParse(settings["rl"], out rl);

   53         int.TryParse(settings["rr"], out rr);

   54 

   55         var tags = settings["Tags"] == null ? new string[0] : settings["Tags"].Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);

   56 

   57         if (tags.Length > 0) GetImage(frequency, tags, dx, dy, rl, rr);

   58     }

   59 

   60     void GetImage(ChangeFrequency changeFrequency, string[] tags, int dx, int dy, int rl, int rr)

   61     {

   62         RandomImageGenerator rig = new RandomImageGenerator();

   63         if (changeFrequency == ChangeFrequency.Daily) rig = new RandomImageGenerator(DateTime.Now.DayOfYear);

   64         else if (changeFrequency == ChangeFrequency.Hourly) rig = new RandomImageGenerator(DateTime.Now.DayOfYear ^ (int)DateTime.Now.TimeOfDay.TotalHours);

   65         else if (changeFrequency == ChangeFrequency.Minutely) rig = new RandomImageGenerator(DateTime.Now.DayOfYear ^ (int)DateTime.Now.TimeOfDay.TotalMinutes);

   66 

   67         foreach (string tag in tags) rig.Tags.Add(tag.Trim());

   68         string[] images = rig.GetRandomImage();

   69 

   70         if (images.Length == 2)

   71         {       

   72             this.PhotoImage.ImageUrl = images[1];

   73             this.PhotoLink.NavigateUrl = images[0];

   74         }

   75         if (dx > 0) this.PhotoImage.Width = dx;

   76         else if (dy > 0) this.PhotoImage.Height = dy;

   77         else this.PhotoImage.Width = 200;

   78     }

   79 }

 

Overriding the “IsEditable” makes an edit link on the widget.

Let’s examine the “LoadWidget” override:

GetSettings() on line 33 returns the settings configuration of the widget in a name/value string dictionary.

Line 35 sets the “frequency”.  I’m setting this to an enumeration that I’ve created in my blog engine assembly.

    1 public enum ChangeFrequency

    2 {

    3     Always,

    4     Daily,

    5     Hourly,

    6     Minutely

    7 }

This is an enumeration I use to determine when to change the random things on my blog, like Quote of the Day, and Random Photo.  “Always” always changes the content when the page is refreshed or a new page is rendered.  “Daily” keeps the random item the same for the entire day.  “Hourly” keeps the random item the same for the current hour, and “Minutely” keeps the item the same for the minute.  Note that this is NOT A DURATION.  i.e. “Daily” will not keep an item good for a 24 hour period.  It will keep a random item constant for the calendar day.  Once the day changes, the random item will be different.

The values dx and dy will be used for the width and height of the thumbnail.  The rr and rl are from an earlier version where I was generating a thumbnail dynamically and rotating it.  It’s not necessary now and should be removed.

The “Tags” value is a comma delimited list of tags from which images will be selected.  Once we have all of the settings parsed and default values in place of missing items, we can get the image.

The “GetImage” method creates a new “RandomImageGenerator” class.  on lines 63 – 65, you can see how the change frequency discussed above keeps a random item constant for a specified period by using a seed derived from the change frequency.  Add the tags on line 67, and get the image names on line 68.  The RandomImageGenerator class will return two strings in an array.  The first string is the URL to the thumbnail image, and the second string is the URL to the full image.

Lines 75 – 77 set the size of the image based on limiting dimensions from the settings.  The aspect ratio of the actual thumbnail will be used, so we only specify the LIMITING side.  If dx is specified, the thumbnail is constrained on the width and is allowed to grow vertically.  If dy is specified, the thumbnail is constrainted on the height, and will grow on the width.  If they are both specified, dy is ignored.

Let’s take a look at the class that gets the random image.

    1 using System;

    2 using System.Collections.Generic;

    3 using System.Linq;

    4 using System.Text;

    5 using System.Configuration;

    6 using System.Text.RegularExpressions;

    7 using System.IO;

    8 using System.Web;

    9 

   10 namespace Funkymule.BlogEngine.Data

   11 {

   12     public class RandomImageGenerator

   13     {

   14         public class DirectorySpec

   15         {

   16             public bool Deep { get; set; }

   17             public string Directory { get; set; }

   18         }

   19 

   20         private static Regex _rx = new Regex(@"<a.*?href=""(?'image'.*?)"".*?>.*?<img.*?src=""(?'thumbnail'.*?)"".*?>.*?</a>", RegexOptions.IgnoreCase | RegexOptions.Compiled);

   21         private static Random _random = new Random();

   22 

   23         private List<string> _tags = new List<string>();

   24 

   25         public IList<string> Tags { get { return _tags; } }

   26 

   27         public RandomImageGenerator()

   28         {

   29         }

   30 

   31         public RandomImageGenerator(int seed)

   32         {

   33             _random = new Random(seed);

   34         }

   35 

   36         public string[] GetRandomImage()

   37         {           

   38             return GetRandomTaggedImage();

   39         }

   40 

   41         private string[] GetRandomTaggedImage()

   42         {

   43             using (BlogEngineDataContext ctx = new BlogEngineDataContext(ConfigurationManager.ConnectionStrings["BlogEngine"].ConnectionString))

   44             {

   45                 // get a random post content that has an image referenced in it

   46 

   47                 // get post ids for all posts with the tag

   48                 var postIds = ctx

   49                     .be_PostTags

   50                     .Where(pt => this.Tags.ToArray().Contains(pt.Tag))

   51                     .Select(pt => pt.PostID);

   52 

   53                 // get a random one

   54                 for (int i = 0; i < 1000; i++)

   55                 {

   56                     Guid postId = postIds.Skip(_random.Next(0, postIds.Count())).First();

   57                     string content = ctx.be_Posts.First(p => p.PostID == postId).PostContent;

   58                     IEnumerable<Match> matches = _rx.Matches(content).Cast<Match>().Where(m => m.Success == true);

   59                     if (matches.Count() > 0)

   60                     {

   61                         Match match = matches.Skip(_random.Next(0, matches.Count())).First();

   62                         return new string[] { match.Groups["image"].Value, match.Groups["thumbnail"].Value };

   63                     }

   64                 }

   65             }

   66 

   67             return new string[0];

   68         }

   69     }

   70 }

It’s not terribly complicated.  A list of tags, a Random that is initialized from the seed specified in the constructor.  Using ADO.NET Entity Framework generated from the BlogEngine database, I am getting a list of post ids where their tag is in the list of specified tags.  Once I have a post, use the regular expression to find an image in it.  Not all posts will have an image, so I look 1000 times to try to find one.  Not elegant, but functional.  The regular expression gets the virtual path to the image, and the thumbnail. 

Note that if the post contains an image that is not a thumbnail that links to a full sized image, this method will not work for you.

 

Edit.ascx

In order to be able to specify parameters to our widget at run time through the web interface, we need to specify an edit control.  The edit control will look like this:

randomphotoconfig

Here, you enter the change frequency, comma delimited list of tags, constraining width, constraining height, and the two fields that are no longer used and need to be removed when I have time.

The ascx source looks like this:

    1 <%@ Control Language="C#" AutoEventWireup="true" CodeFile="edit.ascx.cs" Inherits="widgets_RandomPhoto_edit" %>

    2 <asp:Table runat="server" CellPadding="5">

    3     <asp:TableRow runat="server">

    4         <asp:TableCell runat="server" HorizontalAlign="Right">Change Frequency</asp:TableCell>

    5         <asp:TableCell runat="server">

    6             <asp:DropDownList ID="FrequencyBox" runat="server" />

    7         </asp:TableCell>

    8     </asp:TableRow>

    9     <asp:TableRow runat="server">

   10         <asp:TableCell runat="server" HorizontalAlign="Right">

   11             Tags:<br />

   12             <span style="font-style: italic">Separate multiple tags with a comma.</span>

   13         </asp:TableCell>

   14         <asp:TableCell runat="server">

   15             <asp:TextBox runat="server" ID="TagsTextbox" Width="200" />

   16         </asp:TableCell>

   17     </asp:TableRow>

   18     <asp:TableRow ID="TableRow1" runat="server">

   19         <asp:TableCell ID="TableCell1" runat="server" HorizontalAlign="Right">

   20             Width:<br />

   21         </asp:TableCell>

   22         <asp:TableCell ID="TableCell2" runat="server">

   23             <asp:TextBox runat="server" ID="WidthTextbox" Width="200" />

   24         </asp:TableCell>

   25     </asp:TableRow>

   26     <asp:TableRow ID="TableRow2" runat="server">

   27         <asp:TableCell ID="TableCell3" runat="server" HorizontalAlign="Right">

   28             Height:<br />

   29         </asp:TableCell>

   30         <asp:TableCell ID="TableCell4" runat="server">

   31             <asp:TextBox runat="server" ID="HeightTextbox" Width="200" />

   32         </asp:TableCell>

   33     </asp:TableRow>

   34     <asp:TableRow ID="TableRow3" runat="server">

   35         <asp:TableCell ID="TableCell5" runat="server" HorizontalAlign="Right">

   36             Left Rotation:<br />

   37         </asp:TableCell>

   38         <asp:TableCell ID="TableCell6" runat="server">

   39             <asp:TextBox runat="server" ID="LeftRotationTextbox" Width="200" />

   40         </asp:TableCell>

   41     </asp:TableRow>

   42     <asp:TableRow ID="TableRow4" runat="server">

   43         <asp:TableCell ID="TableCell7" runat="server" HorizontalAlign="Right">

   44             Right Rotation:<br />

   45         </asp:TableCell>

   46         <asp:TableCell ID="TableCell8" runat="server">

   47             <asp:TextBox runat="server" ID="RightRotationTextbox" Width="200" />

   48         </asp:TableCell>

   49     </asp:TableRow>

   50 </asp:Table>

It’s a simple table.  The codebehind:

    1 using System;

    2 using System.Collections.Generic;

    3 using System.Linq;

    4 using System.Web;

    5 using System.Web.UI;

    6 using System.Web.UI.WebControls;

    7 using System.Collections.Specialized;

    8 using Funkymule.BlogEngine.Data;

    9 using System.Globalization;

   10 

   11 public partial class widgets_RandomPhoto_edit : WidgetEditBase

   12 {

   13     protected void Page_Load(object sender, EventArgs e)

   14     {

   15         if (!IsPostBack)

   16         {

   17             int ti;

   18 

   19             FrequencyBox.DataSource = Enum.GetNames(typeof(ChangeFrequency));

   20             FrequencyBox.DataBind();

   21 

   22             FrequencyBox.SelectedValue = ChangeFrequency.Always.ToString();

   23 

   24             StringDictionary settings = GetSettings();

   25             if (settings.ContainsKey("Tags")) TagsTextbox.Text = settings["Tags"];

   26             if (settings.ContainsKey("ChangeFrequency")) FrequencyBox.SelectedValue = settings["ChangeFrequency"];

   27 

   28             if (settings.ContainsKey("dx") && int.TryParse(settings["dx"], out ti)) WidthTextbox.Text = ti.ToString(CultureInfo.InvariantCulture);

   29             if (settings.ContainsKey("dy") && int.TryParse(settings["dy"], out ti)) HeightTextbox.Text = ti.ToString(CultureInfo.InvariantCulture);

   30             if (settings.ContainsKey("rl") && int.TryParse(settings["rl"], out ti)) LeftRotationTextbox.Text = ti.ToString(CultureInfo.InvariantCulture);

   31             if (settings.ContainsKey("rr") && int.TryParse(settings["rr"], out ti)) RightRotationTextbox.Text = ti.ToString(CultureInfo.InvariantCulture);

   32         }

   33     }

   34 

   35     public override void Save()

   36     {       

   37         StringDictionary d = GetSettings();

   38         int tmp;

   39 

   40         d["Tags"] = this.TagsTextbox.Text;

   41         d["ChangeFrequency"] = FrequencyBox.SelectedValue;

   42         if (int.TryParse(WidthTextbox.Text, out tmp)) d["dx"] = tmp.ToString(CultureInfo.InvariantCulture);

   43         else d.Remove("dx");

   44         if (int.TryParse(HeightTextbox.Text, out tmp)) d["dy"] = tmp.ToString(CultureInfo.InvariantCulture);

   45         else d.Remove("dy");

   46         if (int.TryParse(this.LeftRotationTextbox.Text, out tmp)) d["rl"] = tmp.ToString(CultureInfo.InvariantCulture);

   47         else d.Remove("rl");

   48         if (int.TryParse(this.RightRotationTextbox.Text, out tmp)) d["rr"] = tmp.ToString(CultureInfo.InvariantCulture);

   49         else d.Remove("rr");

   50 

   51         SaveSettings(d);

   52     }

   53 }

Lines 19 and 20 populate the combo box with the values of the ChangeFrequency enumeration.  The text of the other controls is set from the values in the settings dictionary.

The “Save” method sets the settings values from the textboxes, etc.

That’s really all there is to it.  I’ve been using it on my blog for a while, and haven’t noticed any problems.  The regular expression might not cover all scenarios, but it should work fine for the most part.  Since my implementation uses a “RandomImageGenerator” class that is in my custom assembly, I haven’t included a project.  The functionality of that class can be merged into the widget.ascx file.  That way, you only need to place the 2 ascx files and 2 ascx.cs files in the widgets/randomPhoto directory and an assembly isn’t required.

randomphoto

Comment on this
Development

Date-aware Theme v2

Tuesday, 07 April 2009 09:16 PM
by Coose

Well, my original Date-aware theme header images was kinda crappy:

  • Required modifications to the BlogEngine.NET theme
  • Only changed a header image
  • Was not implemented as a BlogEngine.NET extension
  • Was not configurable in the BlogEngine.NET control panel
  • …we can go on and on here…it was not a very good solution.

 

So, here is my attempt at version 2. It's quite a bit simpler.

Copy the attached TimelyStyle.cs file into the Extensions folder of the BlogEngine.NET web site.  Now go to the BlogEngine.NET control panel and configure.

More...

Comment on this
Development

Post View Counter

Tuesday, 07 April 2009 07:42 AM
by Coose

My wife wanted to see the number of views she had on each post, so I began the hunt to plug in a view counter for BlogEngine.NET.  I liked the simplicity of this one I found at Moses of Egypt.net.  But after installing, I decided that I wanted to track some information, and I wanted it to go into my SQL database instead of an XML file.

My first pass included a LINQ to Entities model, but due to the artifacts required, that has to be in an assembly (well, it doesn't have to be…but it's easiest that way).  When I started to post it, I though it would be much easier as a simple extension that could be dropped in the BlogEngine.NET extensions directory and go.

So an easier solution to that is a LINQ to SQL context.  It's not going away that soon, so I'm posting the LINQ to SQL solution here.

More...

Comment on this
Development
|

Windows Live Writer and Windows 7

Monday, 30 March 2009 07:48 AM
by Coose

I love using Windows Live Writer instead of BlogEngine.net web editing.  That’s a no-brainer.  But I installed Windows 7 Beta, and Live Writer no workey.  It just crashed on startup.  Others have reported it hanging on startup.

Finally found a simple fix.  It’s pretty obvious, but I’ve had such success with Vista and Windows 7 that I had forgotten about program compatibility.

Right-click the Live Writer shortcut, Select the “Compatibility” tab, check the “Run this program in compatibility mode for” checkbox, and select Windows Vista.

image

Comment on this
Development
|

Date-aware Theme Header Images

Sunday, 29 March 2009 06:56 PM
by Coose

Update: This post is outdated.  It kinda sucks.  Please don't use this.  A much, much, much simpler solution is posted here.

I've always really liked the subtle holiday themed logos that Google puts on their site.  It's not really hard, so I modified my theme to make the header images change on certain days.

It's not terribly complicated, but it shows a few things that might be of some help.

Configuration

The first step is to tell the BlogEngine.NET theme when to change the header image.

I initially was going to take the quick and easy route, and just use the appSettings section of the web.config file.  I really needed three pieces of information for each date-aware header image, so the appSettings really is kind of a hack.

So I created a custom configuration section in my personal BlogEngine.NET assembly.

public class TimelyHeaderConfigurationSection : ConfigurationSection

{

    public TimelyHeaderConfigurationSection()

    {

    }

 

    [ConfigurationProperty("Enabled", IsRequired = false, DefaultValue = true)]

    public bool Enabled

    {

        get { return (bool)this["Enabled"]; }

        set { this["Enabled"] = value; }

    }

 

    [ConfigurationProperty("DefaultUrl", IsRequired = false)]

    public string DefaultUrl

    {

        get { return (string)this["DefaultUrl"]; }

        set { this["DefaultUrl"] = value; }

    }

 

    [ConfigurationProperty("Headers", IsDefaultCollection=false)]

    [ConfigurationCollection(typeof(TimelyHeaderCollection), AddItemName="add", ClearItemsName="clear", RemoveItemName="remove")]

    public TimelyHeaderCollection Headers

    {

        get

        {

            return this["Headers"] as TimelyHeaderCollection;

        }

    }

}

Enabled obviously turns on or off the functionality.

DefaultUrl allows an image URL to be specified in the event that the current date does not have any matching entries.  You could just leave the off and use the default functionality of the theme.  This is really unnecessary…I have plans for using it for something in the future.

Headers is a collection of TimelyHeader configuration elements defining when to display other header images.  The ConfigurationCollection attribute defines what class will handle the collection of child configuration elements.  It is a standard clear/add/remove collection.

 

public class TimelyHeaderCollection : ConfigurationElementCollection

{

    public override ConfigurationElementCollectionType CollectionType

    {

        get

        {

            return ConfigurationElementCollectionType.AddRemoveClearMap;

        }

    }

 

    public TimelyHeaderElement this[int index]

    {

        get { return (TimelyHeaderElement)BaseGet(index); }

        set

        {

            if (BaseGet(index) != null) BaseRemoveAt(index);

            BaseAdd(index, value);

        }

    }

 

    protected override ConfigurationElement CreateNewElement()

    {

        return new TimelyHeaderElement();

    }

 

    protected override object GetElementKey(ConfigurationElement element)

    {

        return ((TimelyHeaderElement)element).Url;

    }

 

    public void Add(TimelyHeaderElement element)

    {

        BaseAdd(element);

    }

 

    public void Clear()

    {

        BaseClear();

    }

 

    public void Remove(TimelyHeaderElement element)

    {

        BaseRemove(element.Url);

    }

 

    public void Remove(string name)

    {

        BaseRemove(name);

    }

 

    public void RemoveAt(int index)

    {

        BaseRemoveAt(index);

    }

}

Nothing interesting or insightful here.

And a simple configuration element:

public class TimelyHeaderElement : ConfigurationElement

{

    [ConfigurationProperty("Url", IsRequired = true)]

    public string Url

    {

        get { return (string)this["Url"]; }

        set { this["Url"] = value; }

    }

 

    [ConfigurationProperty("BeginDate", IsRequired = true)]

    public DateTime BeginDate

    {

        get { return (DateTime)this["BeginDate"]; }

        set { this["BeginDate"] = value; }

    }

 

    [ConfigurationProperty("EndDate", IsRequired = true)]

    public DateTime EndDate

    {

        get { return (DateTime)this["EndDate"]; }

        set { this["EndDate"] = value; }

    }

}

 

Theme Modifications

In order to make the header images (a.k.a. "Timely Headers") work, I had to make some changes to my BlogEngine.NET theme.  My theme is based on the "Inove" BlogEngine theme.

  • Replaced the HTML div, <div id="header">, with an ASP.NET panel, <asp:Panel runat="server" CssClass="header" ID="HeaderControl">.
  • Changed my theme style sheet to change the id header (#header) to a class (.header).
  • Added the following code in the site.master Form_Load method.

TimelyHeaderConfigurationSection section =

    (TimelyHeaderConfigurationSection)ConfigurationManager.GetSection("TimelyHeaders");

 

if (section == null) return;

if (section.Enabled == false) return;

 

// if we have more than one, we want the one that has the LATEST begin date.

TimelyHeaderElement header =

    section

    .Headers

    .Cast<TimelyHeaderElement>().Where(e =>

        e.BeginDate <= DateTime.UtcNow && e.EndDate >= DateTime.UtcNow)

    .OrderBy(e => e.BeginDate)

    .FirstOrDefault();

 

if (header != null)

{

    this.HeaderControl.BackImageUrl = header.Url;

}

else if (!string.IsNullOrEmpty(section.DefaultUrl))

{

    this.HeaderControl.BackImageUrl = section.DefaultUrl;

}

Note that I'm using LINQ, so your web application will have to have LINQ assemblies referenced.

Configure for Use

Register the configuration section in the configSections section:

<section name="TimelyHeaders"

        type="Funkymule.BlogEngine.Configuration.TimelyHeaderConfigurationSection, Funkymule.BlogEngine"/>

Add date ranges for specific headers in the registered configuration section:

<TimelyHeaders Enabled="true">

    <Headers>

        <clear />

        <add Url="~/Images/HamBdayHdr.jpg"

            BeginDate="3/20/2009"

            EndDate="4/3/2009" />

    </Headers>

</TimelyHeaders>

 

Now, when my blog pages load between March 20th and April 3rd, visitors will be greeted with a reminder of my son's birthday.

Note that when two Timely Headers are configured for a specific date, the one that has a BeginDate later than the other is used.

Simple, but effective.

Update: This post is outdated.  It kinda sucks.  Please don't use this.  A much, much, much simpler solution is posted here.

Comment on this
Development

BlogEngine.NET Extension to Clean Up UserControl Tags

Saturday, 28 March 2009 06:18 PM
by Coose

When using the [ usercontrol: …] tag in BlogEngine.NET posts, it really annoys me that they are rendered as literals in the feed "location".  So I wrote this little extension (my first extension, actually), to replace the [ usercontrol: …] tags with something a little better for feeds:

image

(darn you Windows Live Writer!  Why are you continually messing up my thumbnail?)

 

Configured to replace my Media Gallery User Control with the text "View article for this Media Gallery."  It will also accept any other user control file names, and replace with pleasing text.

image 

 

Not very fascinating code:

 

using System;

using System.Collections.Generic;

using System.Linq;

using System.Web;

using BlogEngine.Core.Web.Controls;

using BlogEngine.Core;

using System.Text.RegularExpressions;

using System.Data;

 

[Extension("Fixes [ usercontrol:...] tag in Feeds for configured user controls", "1.0", "Funkymule")]

public class UserControlFeedFixup

{

    private static ExtensionSettings _settings;   

 

    static UserControlFeedFixup()

    {

        Post.Serving += new EventHandler<ServingEventArgs>(Post_Serving);

 

        ExtensionSettings settings = new ExtensionSettings("UserControlFeedFixup");

        settings.AddParameter("UserControlName", "User Control File Name", 255, true);

        settings.AddParameter("ReplacementText", "Replace with", 255, true);

 

        ExtensionManager.ImportSettings(settings);

        _settings = ExtensionManager.GetSettings("UserControlFeedFixup");

    }

 

    private static void Post_Serving(object sender, ServingEventArgs e)

    {

        if (e.Location == ServingLocation.Feed)

        {

            foreach (DataRow row in _settings.GetDataTable().Rows)

            {

                string pattern = @"\[ usercontrol:.*?/User controls/" + row["UserControlName"] + @".*?\]";

                e.Body = Regex.Replace(e.Body, pattern,

                    row["ReplacementText"].ToString(), RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);

            }

        }

    }

}

Note: In order to use the above code, you have to remove the space between the opening square bracket and the word "usercontrol:".  If I spelled it correctly, BlogEngine would try to load a user control here in the code…and we don't want that!

Comment on this
Development

Update to Quote of the Day

Tuesday, 24 March 2009 11:49 AM
by Coose

The Quote of the Day post that I wrote earlier was my first widget for BlogEngine.net.  I quickly was not terribly pleased with it, so I made a few changes.

The biggest changes was the “one quote per day” vs “one quote per request” functionality.  Per request was too many changes…or what I call “shuckin’ and jivin’.”  One per day was not allowing my mindless quotes to be displayed too often.

So I made a change to the configuration to allow the quote to change every request, every minute, every hour, or every day.

So my edit.ascx file was changed to support a drop down combo box for the setting:

<asp:DropDownList ID="FrequencyBox" runat="server" />

and very minor code changes to handle the settings:

protected void Page_Load(object sender, EventArgs e)

{

    if (!Page.IsPostBack)

    {

        FrequencyBox.DataSource = Enum.GetNames(typeof(ChangeFrequency));

        FrequencyBox.DataBind();

 

        FrequencyBox.SelectedValue = ChangeFrequency.Always.ToString();

 

        StringDictionary settings = GetSettings();

        if (settings.ContainsKey("ChangeFrequency"))

        {

            FrequencyBox.SelectedValue = settings["ChangeFrequency"];

        }

    }

}

 

public override void Save()

{

    StringDictionary settings = GetSettings();

    settings["ChangeFrequency"] = FrequencyBox.SelectedValue;

    if (string.IsNullOrEmpty(settings["ChangeFrequency"]))

        settings["ChangeFrequency"] = ChangeFrequency.Always.ToString();

    SaveSettings(settings);

}

and another small change in the data object:

public static Quotation GetNextQuotation(ChangeFrequency frequency)

{

    using (BlogEngineEntities ctx = new BlogEngineEntities(ConnectionString))

    {

        Random r = _random;

 

        switch (frequency)

        {

            case ChangeFrequency.Daily:

                r = new Random(DateTime.Now.DayOfYear);

                break;

            case ChangeFrequency.Hourly:

                r = new Random(DateTime.Now.DayOfYear ^ (int)DateTime.Now.TimeOfDay.TotalHours);

                break;

            case ChangeFrequency.Minutely:

                r = new Random(DateTime.Now.DayOfYear ^ (int)DateTime.Now.TimeOfDay.TotalMinutes);

                break;

            case ChangeFrequency.Always:

                // just leave r equal to the _random

                break;

        }

 

        return ctx.QuotationSet.OrderBy(q => q.Added)

            .Skip(r.Next(0, ctx.QuotationSet.Count() - 1)).First();

    }

}

This should make the widget a little more flexible.  Using different random seeds ensures that the first “random” number in the sequence will be consistent with the same seed, and means I don’t have to maintain a timer, or state from previous requests.

Comment on this
Development

Another Quote of the Day for BlogEngine.NET

Sunday, 22 March 2009 12:26 AM
by Coose

Yeah, yeah.  It’s been done before.  I know.  I wanted a few things that I couldn’t find with a few minutes of Googling.  And if it takes more than a few minutes, I lose interest.

I wanted to learn to create a BlogEngine.net widget, I wanted the quotes stored in SQL Server, and I wanted to use Entity Framework for the data access.  The others I have seen are pretty cool, but I don’t really like storing the data in the freeform StringDictionary values that go with the widget framework.

It turns out that creating a widget with BlogEngine.net is pretty easy.  There should be hundreds of tutorials around, so I won’t bore anyone with the details.

Database Schema

QuoteSchema

Simple schema.  Just a little example of what I am doing.

Data Objects

I typically use Linq to Sql in these types of small apps, but it’s been announced that Linq to Sql is “on its way out” and EF is the “preferred” method of access.  Since I use a custom code-generated EF framework at work, I went ahead and wrote this widget using EF.

I have an assembly for all my BlogEngine.net stuff, so I added the quotes data objects into that assembly.  Any referenced assembly (or App_Code, etc) will work.

QuoteIde

Again, nothing complicated here.  Changed up some names.

Since I would be using ASP ObjectDataSource, I needed some static methods for access.  Here’s the class I used.

 

public class Quotations

{

    private static Random _random = new Random();

 

    private static string ConnectionString

    {

        get

        {

            return string.Format(

                "metadata=res://*/Data.QuotationModel.csdl|" +

                "res://*/Data.QuotationModel.ssdl|" +

                "res://*/Data.QuotationModel.msl;" +

                "provider=System.Data.SqlClient;" +

                "provider connection string=\"{0}\"",

            ConfigurationManager

                .ConnectionStrings["BlogEngine"].ConnectionString);

        }

    }

 

    public static Quotation GetNextQuotation(bool instancePerDay)

    {

        using (BlogEngineEntities ctx = new BlogEngineEntities(ConnectionString))

        {

            Random r = _random;

 

            if (instancePerDay)

            {

                r = new Random(DateTime.Now.DayOfYear);

            }

 

            return ctx.QuotationSet.OrderBy(q => q.Added)

                .Skip(r.Next(0, ctx.QuotationSet.Count() - 1)).First();

        }

    }

 

    public static void Insert(Quotation quote)

    {

        using (BlogEngineEntities ctx = new BlogEngineEntities(ConnectionString))

        {

            ctx.AddToQuotationSet(quote);

            ctx.SaveChanges(true);

        }

    }

 

    public static void Save(Quotation quote)

    {

        using (BlogEngineEntities ctx = new BlogEngineEntities(ConnectionString))

        {

            ctx.Attach(quote);

            ctx.SaveChanges(true);

        }

    }

 

    public static int GetCount()

    {

        using (BlogEngineEntities ctx = new BlogEngineEntities(ConnectionString))

        {

            return ctx.QuotationSet.Count();

        }

    }

 

    public static Quotation[] Get()

    {

        using (BlogEngineEntities ctx = new BlogEngineEntities(ConnectionString))

        {

            return ctx.QuotationSet.ToArray();

        }

    }

 

    public static Quotation[] Get(int start, int count)

    {

        using (BlogEngineEntities ctx = new BlogEngineEntities(ConnectionString))

        {

            return ctx.QuotationSet.OrderBy(q => q.Added)

                .Skip(start).Take(count).ToArray();

        }

    }

}

 

 

 

There’s not much of note about the code.  The only thing worthy of note is the code:

if (instancePerDay)

{

    r = new Random(DateTime.Now.DayOfYear);

}

 

What I’m doing here is using the “instancePerDay” option to make sure the quote stays the same all day.  If this option is true, the quote will be the same on every request all day.  If it is false, the quote will randomize on each page request.

By using the Random object with a seed of the current day of year, the random number generator will give me the same sequence all day.

Update: I’ve made this more flexible, allowing for changes per day, hour, or minute.  Code changes are posted here.

 

The Widget

The widget is a simple .aspx file with two labels.  Overriding the LoadWidget() method:

public override void LoadWidget()

{

    LoadSettings();

    Quotation quote = Quotations.GetNextQuotation(_instancePerDay);

 

    lblQuotation.Text = Server.HtmlEncode(quote.Quote);

    lblQuotationAuthor.Text = Server.HtmlEncode(quote.Author);

}

 

private void LoadSettings()

{

    StringDictionary sd = GetSettings();

 

    if (!bool.TryParse(sd["InstancePerDay"], out _instancePerDay))

        _instancePerDay = false;

}

 

You could also use Literals with a mode of encode, or use the Anti Cross Scripting library from Microsoft…but values from the database should always be encoded.

 

The Edit Control

The widget returns true from the IsEditable property, so I need to provide an editor.  I used databinding, so loading and saving the actual data is handled by the data object described above.

The asp object data source looks like this:

<asp:ObjectDataSource EnableCaching="true" ID="QuoteSource"

    runat="server" DataObjectTypeName="Funkymule.BlogEngine.Data.Quotation"

    TypeName="Funkymule.BlogEngine.Data.Quotations"

    ConvertNullToDBNull="true" InsertMethod="Insert" SelectCountMethod="GetCount"

    SelectMethod="Get" UpdateMethod="Save" EnablePaging="true"

    StartRowIndexParameterName="start" MaximumRowsParameterName="count" />

 

The TypeName is the data object we created above.  The *Method and *ParameterName properties match the methods from that object.  The DataObjectTypeName is the data object that I use as a parameter to the methods, so I don’t have to provide one parameter for each property.  In this case, it’s not that big of deal since there are so few properties…but again, this is an exercise in learning, so I tried to do it the right way.  The implementation of the DataObjectTypeName is the Entity Framework generated class.

Now, the code behind the control is simple:

protected void Page_Load(object sender, EventArgs e)

{

    if (!Page.IsPostBack)

    {

        bool instance = false;

 

        StringDictionary settings = GetSettings();

        if (settings.ContainsKey("InstancePerDay"))

        {

            bool.TryParse(settings["InstancePerDay"], out instance);

        }

 

        InstancePerDayCheckbox.Checked = instance;

    }

}

 

public override void Save()

{

    StringDictionary settings = GetSettings();

    settings["InstancePerDay"] = InstancePerDayCheckbox.Checked.ToString();

    SaveSettings(settings);

}

 

The only thing of interest here is that I have one value which is actually a setting, and not part of the quote data.  This value is not databound, so I’m handling it all manually.

Anyway…when it’s all said and done…here’s the results:

QuotePreview

QuoteList

 

Take it for what it’s worth.

Check the updates here.

--Coose

Comment on this
Development

Funkymule.com has changed...again

Tuesday, 17 March 2009 01:40 PM
by Coose

Well, I finally had enough of DotNetNuke.  It's an OK platform, but as a professional developer and architect, I would hardly say it's great.  What a bloated, cumbersome, difficult (and not pretty) platform to work with.

So, now I'm moving everything to Graffiti.  Here's my review:

Update: March 21st.

Yeah...Graffiti sucks.  It has a lot of potential...but there are a few reasons that I moved off of it after 2 days:

  • Development appears to have stopped.  They removed their roadmaps, and stopped posting to their own forums.
  • Templating engine required a lot of propietary macros and .view files...dot really .NET friendly.
  • UserControls were not easily implemented.  I was able to write a plugin to load any UserControl from an .ascx file, but due to the way it was hooked up, Postbacks and ASP.NET AJAX would not work.
  • Closed source.

There's a few more reasons that I moved off of it...but I don't want to trash the product.  It had great potential.

So anyway...I moved to BlogEngine.net.  I wrote a program to manually read the Graffiti SQL tables, and insert into the BlogEngine.net SQL tables. (I didn't want to use the default xml file provider).

Let's see how long till I change again!

Comment on this
Mike's Blog | Development