Steam Achievements: A Facebook app built with WCF and jQuery

by jrummell 23. December 2009 23:49

I’ve been wanting to create a Facebook app for a while now, but I didn’t have time or a need to fill. I took some time off over the last week and I was able to come up with both. I’m avid PC gamer and most of my games are on Valve’s Steam platform. Steam has achievements for some of its biggest newer games like Left 4 Dead and Team Fortress 2. They are much like the Xbox 360 and PS3 achievements. But unlike the Xbox and PS3, there wasn’t a Facebook app that notifies your friends with your latest achievements. There was an app that worked great for a while back, but its now broken (and still using the steamachievements url suffix!).

I’ve created an app at http://www.facebook.com/apps/application.php?id=211407042025.

I first tried an FBML Canvas app built with ASP.NET MVC and the MS Facebook SDK, but I couldn’t find a complete working example. Then I tried FBML with ASP.NET WebForms (traditional ASP.NET) but testing was super annoying since it had to be run in the context of Facebook and I couldn’t get FBJS Ajax to work. Then finally, I tried an IFrame Canvas app built with ASP.NET WebForms. I could now test easier and use jQuery instead of FBJS! For more information, see this wiki topic on IFrame vs. FBML Canvas apps.

Now that I was in familiar territory, I threw a wrench in the mix by using WCF web services instead of asmx. Not that WCF is better/worse than asmx, I had just never used them before. I had some help from Rick Strahl’s blog post called jQuery AJAX calls to a WCF REST Service. This is was no easy task for me, so I’ll list all of the details here in case anyone else is struggling. First, here is the service interface:

[ServiceContract]
public interface IAchievementService
{
    [OperationContract]
    [WebInvoke(Method = "POST", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json, BodyStyle = WebMessageBodyStyle.WrappedRequest)]
    List<Achievement> GetAchievements(string steamUserId, int gameId);

    [OperationContract]
    [WebInvoke(Method = "POST", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json, BodyStyle = WebMessageBodyStyle.WrappedRequest)]
    List<Game> GetGames();

    [OperationContract]
    [WebInvoke(Method = "POST", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json, BodyStyle = WebMessageBodyStyle.WrappedRequest)]
    bool UpdateAchievements(string steamUserId);

    [OperationContract]
    [WebInvoke(Method = "POST", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json, BodyStyle = WebMessageBodyStyle.WrappedRequest)]
    bool UpdateSteamUserId(long facebookUserId, string steamUserId);

    [OperationContract]
    [WebInvoke(Method = "POST", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json, BodyStyle = WebMessageBodyStyle.WrappedRequest)]
    bool PublishLatestAchievements(long facebookUserId, string steamUserId);
}

Note the WCF attributes: ServiceContract and OperationContract. These kind of correspond to WebService and WebMethod in an asmx web service. I say kind of because a WCF service doesn’t have to be a web service, but for this example it is. The WebInvoke attribute tells ASP.NET how the client will interact with the service. The method is POST, and both the request and the response will be json. The WrappedRequest BodyStyle is what allows you to send a json JavaScript object to a service method that gets de-serialized into parameters. If you were to omit this, you’d have to create a class for your parameters and change the method signature to accept one instance of that class. Another thing to note is that I’m returning List<T> instead of IEnumerable<T>. This is because there seems to be a serialization bug with WCF and IEnumerable<T>.

The implementation of IAchievementService is very straight forward. It is simply hands all of the heavy lifting to a manager class that uses LINQ to SQL to communicate with the database. The next part is the WCF configuration in web.config.

  <system.serviceModel>
    <services>
      <service behaviorConfiguration="SteamAchievements.Services.AchievementServiceBehavior" name="SteamAchievements.Services.AchievementService">
        <endpoint address="" binding="webHttpBinding" contract="SteamAchievements.Services.IAchievementService" behaviorConfiguration="SteamAchievements.Services.AchievementServiceBehavior" />
        <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="SteamAchievements.Services.AchievementServiceBehavior">
          <serviceMetadata httpGetEnabled="True" />
          <serviceDebug includeExceptionDetailInFaults="True" />
        </behavior>
      </serviceBehaviors>
      <endpointBehaviors>
        <behavior name="SteamAchievements.Services.AchievementServiceBehavior">
          <webHttp />
        </behavior>
      </endpointBehaviors>
    </behaviors>
    <serviceHostingEnvironment aspNetCompatibilityEnabled="false">
      <baseAddressPrefixFilters>
        <add prefix="http://www.rummell.info/" />
      </baseAddressPrefixFilters>
    </serviceHostingEnvironment>
  </system.serviceModel>

This is fairly standard except for a few things. I’m using the webHttp endpoint behavior to enable AJAX and I also had to add a baseAddressPrefixFilter to get it working on my web host.

Finally, here is the JavaScript that calls the WCF service methods:

var parameters = { "steamUserId": steamUserId, "gameId": gameId };
callAjax("GetAchievements", parameters, ondone);

function callAjax(method, query, ondone)
{
    var onerror = function(m)
    {
        $("#log").text(m.Message).show();
    };

    $.ajax({
        url: _serviceBase + method,
        data: JSON.stringify(query),
        type: "POST",
        processData: true,
        contentType: "application/json",
        timeout: 10000,
        dataType: "json",
        success: ondone,
        error: function(xhr)
        {
            if (!onerror)
            {
                return;
            }

            if (xhr.responseText)
            {
                try
                {
                    var err = JSON.parse(xhr.responseText);
                    if (err)
                    {
                        onerror(err);
                    }
                    else
                    {
                        onerror({ Message: "Unknown server error." });
                    }
                }
                catch (e)
                {
                    onerror({ Message: e.toString() });
                }
            }
            return;
        }
    });
}

This is basically the same snippet from Rick Strahl (see link above) that I’ve modified slightly to fit my application.

One final note on using the Facebook API from the context of a web service. The PublishLatestAchievements service method uses the API to publish achievements to the user’s profile. Since its outside the scope of a page, you can’t use Master.Api. To get around this, you’ll need to create an instance of CanvasSession:

string appKey = WebConfigurationManager.AppSettings["APIKey"];
string appSecret = WebConfigurationManager.AppSettings["Secret"];
List<Enums.ExtendedPermissions> permissions =
    new List<Enums.ExtendedPermissions> {Enums.ExtendedPermissions.publish_stream};
CanvasSession session = new IFrameCanvasSession(appKey, appSecret, permissions, false);
Api api = new Api(session);

Then you can publish to the stream:

api.Stream.Publish(description, attachment, links, null, facebookUserId);

Most samples don’t include the uid parameter (facebookUserId), but its required here since HttpSession is unavailable.

If you’d like to see more, take a look at the Google Code project.

Tags: , ,

asp.net

Mocking Controller.User

by jrummell 17. September 2009 21:26

I’m currently working on my first ASP.NET MVC project. Naturally, I’m writing a good number of unit tests. I ran into a problem tonight with mocking Controller.User. Thankfully, someone at Stack Overflow had already asked a question about this. I took Bruno Reis’ answer:

var principal = new Moq.Mock<IPrincipal>();
// ... mock IPrincipal as you wish

var httpContext = new Moq.Mock<HttpContextBase>();
httpContext.Setup(x => x.User).Returns(principal.Object);
// ... mock other httpContext's properties, methods, as needed

var reqContext = new RequestContext(httpContext.Object, new RouteData());

// now create the controller:
var controller = new MyController();
controller.ControllerContext =
    new ControllerContext(reqContext, controller);

and I rewrote it using NUnit.Mocks and wrapped it into an implementation of HttpContextBase:

/// <summary>
/// A mock <see cref="HttpContextBase"/> that implements <see cref="HttpContextBase.User"/>.
/// </summary>
public class MockHttpContext : HttpContextBase
{
    private IPrincipal _user;

    public MockHttpContext()
    {
        DynamicMock identity = new DynamicMock(typeof (IIdentity));
        identity.ExpectAndReturn("get_Name", "testUser");

        DynamicMock user = new DynamicMock(typeof (IPrincipal));
        user.ExpectAndReturn("get_Identity", identity.MockInstance);

        _user = (IPrincipal) user.MockInstance;
    }

    public override IPrincipal User
    {
        get { return _user; }
        set { _user = value; }
    }
}

Now mocking Controller.User is as easy as this:

// create an instance of RequestContext using MockHttpContext.
RequestContext requestContext =
    new RequestContext(new MockHttpContext(), new RouteData());

// initialize the controller's ControllerContext
_controller.ControllerContext = new ControllerContext(requestContext, _controller);

Tags: ,

asp.net

Apply the same CSS class to all validators in a web project

by jrummell 16. August 2009 14:33

I recently had to add a CSS class to all validators in an ASP.NET web application.  I started with the theme’s skin file:

<asp:CompareValidator runat="server" 
    CssClass="error" />
<asp:CustomValidator runat="server" 
    CssClass="error" />
<asp:RequiredFieldValidator runat="server" 
    CssClass="error" />
<belCommon:ZipCodeValidator runat="server" 
    CssClass="error" />
<belCommon:PhoneNumberValidator runat="server" 
    CssClass="error" />

But what if I decide to use another validator down the road? I would have to remember to add it to the skin. Knowing that I was bound to forget, I sought out another method. After doing some digging, I found that ASP.NET generates a JavaScript variable called Page_Validators. This is an array of all the validator span elements on the current page. Now that I have access to the spans, I could write a script in the site’s Master Page to apply the class:

if (Page_Validators != null)
{
    for (i = 0; i < Page_Validators.length; i++)
    {
        Page_Validators[i].className = "error";
    }
}

To have it run when the page is loaded, I added it as an Sys.Application.init handler:

Sys.Application.add_init(function(sender, args)
{
    if (Page_Validators != null)
    {
        for (i = 0; i < Page_Validators.length; i++)
        {
            Page_Validators[i].className = "error";
        }
    }
});

You could also use jQuery’s document.ready handler:

$(document).ready(function()
{
    if (Page_Validators != null)
    {
        for (i = 0; i < Page_Validators.length; i++)
        {
            Page_Validators[i].className = "error";
        }
    }
});

Tags: ,

asp.net

xVal with WebForms Part 2

by jrummell 12. August 2009 22:17

Since my last post, I’ve completely rethought and re-implemented my take on xVal for WebForms. If you’re not familiar with xVal, stop now and read the tutorial. Now that you’re back, lets talk about xVal and WebForms.

Model

This is the model we’ll be using (you should recognize it from the xVal tutorial):

public class Booking
{
    [Required]
    [StringLength(15)]
    public string ClientName { get; set; }

    [Range(1, 20)]
    public int NumberOfGuests { get; set; }

    [Required]
    [DataType(DataType.Date)]
    public DateTime ArrivalDate { get; set; }
}

Form

And here is the form:

<asp:ValidationSummary ID="valSummary" runat="server" />
<label for="txtClientName">
    Your name:</label>
<asp:TextBox ID="txtClientName" runat="server" />
<label for="txtNumberOfGuests">
    Number of guests:</label>
<asp:TextBox ID="txtNumberOfGuests" runat="server" />
<label for="txtArrivalDate">
    Arrival date:</label>
<asp:TextBox ID="txtArrivalDate" runat="server" />
<asp:Button ID="btnSubmit" runat="server" Text="Submit" OnClick="btnSubmit_Click" />

ModelValidator

My first try at validation was adding a validator control for each input field. After playing with it a bit, I decided that it would be better to have one validator for the entire model. This control defines the model’s type (ModelType), and then maps each property (PropertyName) to an input control (ControlToValidate).

<val:ModelValidator ID="valBooking" runat="server" ModelType="xVal.WebForms.Demo.Booking, xVal.WebForms.Demo">
    <ModelProperties>
        <val:ModelProperty PropertyName="ClientName" ControlToValidate="txtClientName" />
        <val:ModelProperty PropertyName="NumberOfGuests" ControlToValidate="txtNumberOfGuests" />
        <val:ModelProperty PropertyName="ArrivalDate" ControlToValidate="txtArrivalDate" />
    </ModelProperties>
</val:ModelValidator>

ControlToValidate

The biggest challenge was figuring out how to reference the input controls by given ID instead of by the elementPrefix + PropertyName convention. In other words, MVC xVal assumes that your ClientName input control ID is booking.ClientName, where booking is the elementPrefix and ClientName is the name of the property. This doesn’t work out so well with web forms and generated IDs. I got around this with the ModelProperties collection of ModelValidator. Then I updated the json formatted rule script to include each property’s control ID.

Complete Source and Demo

Get the complete source (with Bookings demo) at the xVal.WebForms project page on CodePlex.

Tags: , ,

asp.net

xVal with WebForms

by jrummell 15. July 2009 22:32

Update: See xVal with WebForms Part 2 for a better implementation.

What is xVal and why would anyone want to use it?

xVal is a validation framework for ASP.NET MVC applications. It makes it easy to link up your choice of server-side validation mechanism with your choice of client-side validation library, neatly fitting both into ASP.NET MVC architecture and conventions.

See the CodePlex page for more information.

Basically, what xVal does, is take your validation rules and perform server and client side validation based on those rules. That means you don’t have to duplicate model validation at the page level. Now you’re probably thinking, “Isn’t it for MVC?”.  It is. But I, and at least two others, would like to take advantage of xVal’s features in traditional ASP.NET WebForm projects.

Getting it to work with WebForms

I finally found some time last night to see what it would take to get xVal working in an ASP.NET Web Application Project.  After a few hours I had something. I only needed to add two classes on top of xVal, DataAnnotationsValidationRunner and ModelValidator.

public static class DataAnnotationsValidationRunner
{
    public static IEnumerable<ErrorInfo> GetErrors(object instance, string propertyName)
    {
        return from prop in TypeDescriptor.GetProperties(instance).Cast<PropertyDescriptor>()
               from attribute in prop.Attributes.OfType<ValidationAttribute>()
               where prop.Name == propertyName && !attribute.IsValid(prop.GetValue(instance))
               select new ErrorInfo(prop.Name, attribute.FormatErrorMessage(string.Empty), instance);
    }
}

This is based on the implementation in the xVal demo. I added a second parameter to GetErrors() that allows the runner to check a specific property.

 

public class ModelValidator : BaseValidator
{
    private ValidationInfo _validationInfo;

    public string ModelType
    {
        get { return (string) ViewState["ModelType"]; }
        set { ViewState["ModelType"] = value; }
    }

    public string ModelProperty
    {
        get { return (string) ViewState["ModelProperty"]; }
        set { ViewState["ModelProperty"] = value; }
    }

    protected override bool EvaluateIsValid()
    {
        Type type = Type.GetType(ModelType);

        object model = Activator.CreateInstance(type);

        IEnumerable<ErrorInfo> errors = DataAnnotationsValidationRunner.GetErrors(model, ModelProperty);

        StringBuilder errorBuilder = new StringBuilder();
        foreach (ErrorInfo error in errors)
        {
            errorBuilder.AppendLine(error.ErrorMessage);
        }

        ErrorMessage = errorBuilder.ToString();

        return ErrorMessage.Length > 0;
    }

    protected override void Render(HtmlTextWriter writer)
    {
        Type type = Type.GetType(ModelType);
        _validationInfo = new ValidationInfo(ActiveRuleProviders.GetRulesForType(type), String.Empty);

        writer.Write(_validationInfo.ToString());
    }
}

This is the ASP.NET WebForms version of <%= Html.ClientSideValidation<Booking>("booking") %>, an implementation of BaseValidator. It uses ValidationInfo for rendering the client validation script and DataAnnotationsValidationRunner for the server side validation.

You can use it like this:

public class Customer
{
    [Required, StringLength(20)]
    public string Name { get; set; }
}
<script type="text/javascript" src="xVal.AspNetNative.js"></script>
<div>
    <asp:Label ID="lblName" runat="server" AssociatedControlID="Name">Customer Name:</asp:Label>
    <asp:TextBox ID="Name" runat="server" />
    <val:ModelValidator ID="validator" runat="server" ModelType="xVal.WebForms.Test.Customer, xVal.WebForms.Test"
        ModelProperty="Name" ControlToValidate="Name" />
</div>
<asp:Button ID="btnSubmit" runat="server" Text="Submit" />

There are a few things to note here:

  • The TextBox ID must be the same as the model’s property name. This is because the xVal javascript is expecting the control’s ID to be PropertyName or prefix.PropertyName (MVC naming conventions). This could probably be fixed by modifying the client side plugins.
  • The ControlToValidate property on ModelValidator doesn’t do anything, but it’s required by any implementation of BaseValidator (it will throw an exception if you omit it). This could probably be avoided by having ModelValidator inherit from Control and implement IValidator instead.
  • xVal depends on System.Web.Mvc. It uses the TagBuilder and ModelState classes in a few places. There’s really now way around this without refactoring the MVC specific stuff into a separate assembly.

I’ll post an update when I have a chance to work on the first two items.

Tags: ,

asp.net

Send a Completed Form Email Without a StringBuilder

by jrummell 29. June 2009 23:09

Have you ever had to create a large web form for users to fill out and then receive an email copy after its submitted? That can be tedious work. The first few times I did it, I used a StringBuilder to build the email HTML one control at a time. Later, I viewed the HTML output of the page and replaced all input controls with spans, and then put that HTML in a StringBuilder. Either of these methods work, but it gets real annoying when I later have to add a field or two to the form and therefore to the email HTML.

I knew there had to be a way to do this programmatically without copying and pasting into a StringBuilder. Well, there is. Here’s a rather common code snippet that does just this:

public static string GetRenderedHtml(this Control control)
{
StringBuilder sbHtml = new StringBuilder();
using (StringWriter stringWriter = new StringWriter(sbHtml))
using (HtmlTextWriter textWriter = new HtmlTextWriter(stringWriter))
{
control.RenderControl(textWriter);
}
return sbHtml.ToString();
}

This is great! Let’s try it out on this simple example:

<div id="divForm" runat="server">
<fieldset class="inputArea">
<legend>Contact</legend>
<asp:Label runat="server" AssociatedControlID="txtName">
Name</asp:Label>
<asp:TextBox runat="server" ID="txtName" />
<asp:Label runat="server" AssociatedControlID="txtEmail">
Email</asp:Label>
<asp:TextBox runat="server" ID="txtEmail" />
<asp:Label runat="server" AssociatedControlID="txtWebsite">
Website</asp:Label>
<asp:TextBox runat="server" ID="txtWebsite" />
<asp:Label runat="server" AssociatedControlID="txtComment">
Comment</asp:Label>
<asp:TextBox runat="server" ID="txtComment" TextMode="MultiLine" Rows="4" cols="30" />
<asp:Button ID="btnSubmit" runat="server" Text="Submit" OnClick="btnSubmit_Click" />
</fieldset>
</div>

 

protected void btnSubmit_Click(object sender, EventArgs e)
{
txtRenderedHtml.Text = divForm.GetRenderedHtml();
}

Here is what we get:

Control 'txtName' of type 'TextBox' must be placed inside a form tag with runat=server.

So how do you get around that? Well, lets think about this. I’m trying to capture a form and render it as HTML to be included in an email, so I don’t want any TextBoxes. Lets replace the TextBoxes (and any other editable controls) with Labels and try again.

public static void ReplaceEditableControls(this Control control)
{
// don't bother with controls that aren't visible
if (!control.Visible)
{
return;
}
ListControl listControl = control as ListControl;
IButtonControl buttonControl = control as IButtonControl;
IValidator validator = control as IValidator;
IEditableTextControl textControl = control as IEditableTextControl;
UpdatePanel updatePanel = control as UpdatePanel;
if (validator != null || buttonControl != null)
{
control.Visible = false;
}
else if (listControl != null && listControl.SelectedItem != null)
{
Label label = new Label {Text = listControl.SelectedItem.Text, CssClass = "text"};
Replace(listControl, label);
}
else if (textControl != null)
{
Label label = new Label {Text = textControl.Text, CssClass = "text"};
Replace((Control) textControl, label);
}
else if (updatePanel != null)
{
// replace the update panel with a place holder
PlaceHolder holder = new PlaceHolder();
Control[] panelControls = new Control[updatePanel.ContentTemplateContainer.Controls.Count];
updatePanel.ContentTemplateContainer.Controls.CopyTo(panelControls, 0);
foreach (Control panelControl in panelControls)
{
holder.Controls.Add(panelControl);
}
ReplaceEditableControls(holder);
Replace(updatePanel, holder);
}
else if (control.HasControls())
{
Control[] controlsCopy = new Control[control.Controls.Count];
control.Controls.CopyTo(controlsCopy, 0);
foreach (Control controlCopy in controlsCopy)
{
ReplaceEditableControls(controlCopy);
}
}
}

There are a few things to note here.

  • The check for ListControl is before IEditableTextControl because of the way it implements IEditableTextControl. ListControl.Text returns ListControl.SelectedValue, but ListControl.SelectedItem.Text makes more sense.

  • UpdatePanels are a special case because of ContentTemplate. They are replaced with a PlaceHolder and then the method is recursively called on each child control.

  • Finally, if the control has a control collection of its own, a recursive call is made on each child control.

  • Notice that the control collection is copied to an array before making the recursive call. This is because the control collection is modified and you can’t modify a collection while iterating it. Well, you can, but you will have problems.

Now we can change the button handler to:

protected void btnSubmit_Click(object sender, EventArgs e)
{
divForm.ReplaceEditableControls();
}
Which will render the following HTML:
<div id="divForm">
<fieldset class="inputArea">
<legend>Contact</legend>
<label for="txtName">
Name</label>
<span id="txtName" class="text">John Rummell</span>
<label for="txtEmail">
Email</label>
<span id="txtEmail" class="text">jrummell@example.com</span>
<label for="txtWebsite">
Website</label>
<span id="txtWebsite" class="text">john.rummell.info</span>
<label for="txtComment">
Comment</label>
<span id="txtComment" class="text">Check out this new post!</span>
</fieldset>
</div>

To capture this as a string, just add a call to GetRenderedHtml:

protected void btnSubmit_Click(object sender, EventArgs e)
{
divForm.ReplaceEditableControls();
string html = divForm.GetRenderedHtml();
//TODO: send email
}

 

 

(The form style is a slight variation of Janko’s tutorial)

Tags:

asp.net

jQuery/ASP.Net AJAX 1.0/3.5 gotcha

by jrummell 18. December 2008 22:39

Update: If you are curious as to why MS added the .d attribute, find out why at Encosia.

I was very frustrated the other day trying to figure out why a jQuery ajax call worked on my dev box but not on the server.  It looked something like this:

json(_serviceUrl, "{}", true,
    function(result) { fillSelect($("#ddlDepartment")[0], result.d); },
    function(ajax) { /* handle error */ });
    
// calls a json web service
function json(url, data, async, onSuccess, onFailed)
{
    $.ajax({
        async: async,
        type: "POST",
        url: url,
        data: data,
        contentType: "application/json; charset=utf-8",
        dataType: "json",
        success: onSuccess,
        error: onFailed
    });
}

(The code inside the json function is straight from Dave Ward's post on Using jQuery to Consume ASP.NET JSON Web Services. Dave's blog is an excellent resource full of information on jQuery, AJAX, ASP.Net and how to make them play nice together.)

I figured out the problem was result.d; on the server it was null, but on my machine it was a ListItem[], as expected. It turns out that on my machine, the web service at _serviceUrl was compiled against .Net 3.5. while on the server it was compiled against .Net 2.0 with ASP.NET AJAX Extensions 1.0. In order to get it working on the server, I had to change result.d to result. Apparently they changed a few things in System.Web.Extensions v3.5. Unfortunately, I'm unable to install .Net 3.5 on the server. So I came up with this helper function to get things working in both 1.0 and 3.5:

// gets the ajaxResult. Returns ajaxResult.d is if it is not null, else ajaxResult.
// System.Web.Extensions v3.5 web services will result in ajaxResult.d while v1.0 will be ajaxResult.
function getResult(ajaxResult)
{
    return ajaxResult.d == null ? ajaxResult : ajaxResult.d;
}

So now after replacing result with getResult(result), the json call looks like this:

json(_serviceUrl, "{}", true,
    function(result) { fillSelect($("#ddlDepartment")[0], getResult(result)); },
    function(ajax) { /* handle error */ });

Hopefully this will save someone out there some frustration!

Tags: ,

asp.net

WebIconProvider

by jrummell 21. August 2008 22:11

How do you handle sharing icon sets across your web projects?  I started out with an icon folder in each project.  Keeping folders up to date was a bit of a pain - having to remember to update each change across all projects.  Eventually I moved the icons into a class library and compiled them as web resources.  This was great because I could access them from any web project.  But then I wanted to use a different icon set for one project.  I could have just replaced the library icons with the new set, but I didn't want the new set for all of web projects.  All of my projects use the same kinds of icons, e.g. save, edit, delete, new, comment, etc.  So then it hit me, "What if I had an icon set interface that could be used to plug in any number of different icon sets?"

 

Provider Model

The Provider Model seemed like the logical approach.  I had already worked with it when creating custom Membership and Role providers, as well as a SiteMap provider.  The great thing about the provider model is that you can swap providers without recompiling and even do it at runtime.  Here's a class diagram of my provider implementation.

Provider Classes

Following the provider model, I have the following four classes:

WebIconProvider is the abstract icon ProviderBase class that does most of the heavy lifting.  The most important method here is GetImageUrl(WebIcon icon). This an abstract method that returns the url of an image based on the given WebIcon enum value.  WebIcon contains all of the required image types (save, edit, delete, new, comment, etc).

WebIconProviderCollection is a strongly typed collection of WebIconProviders.

WebIconService is a static class that gives access to the providers and a few methods that perform operations using the default provider.

WebIconSection is the ConfigurationSection implementation that contains the configuration settings required for WebIconProviders.

 

WebIconProvider

Now the next step is to implement WebIconProvider.  After implementing it with a few different icon sets, I realized I could refactor out two more abstract classes.

WebIconProvider Classes 

FileWebIconProvider is a virtual path based implementation that provides an ImagePath property that is set in the provider's configuration.  The implementing class must provide the filename of each WebIcon image when it implements WebIconProvider.GetImageUrl(WebIcon icon).

ResourceWebIconProvider is a WebResource implementation that provides a static GetImageUrl(Type type, string resourceName) method that handles retrieving the web resource url.  The implementing class must provide an implementation of IIconResources in it's constructor.  IIconResources is an interface that defines a web resource url for each WebIcon enum value.  ResourceWebIconProvider also provides an implementation of WebIconProvider.GetImageUrl(WebIcon icon) that maps WebIcon values to the appropriate IIconResources property.

 

ResourceWebIconProvider

FileWebIconProvider is pretty much self explanatory.  Creating a new ResourceWebIconProvider icon set is also fairly simple.  First you need to pick out your icons! A few free sets that I like are Silk Icons, Sancons, and ASP.Net Icons. Then you'll need to add them to a class library project and set their build action to Embedded Resource.  The next step is to implement IIconResources.

    /// <summary>
    /// The SilkIcon implementation of <see cref="IIconResources"/>.
    /// </summary>
    internal struct SilkIconResources : IIconResources
    {
        private const string __baseResourcePath = "SilkIcons.Icons.";
        internal const string _Add = __baseResourcePath + "add.png";
        internal const string _Calendar = __baseResourcePath + "calendar.png";
        internal const string _Check = __baseResourcePath + "tick.png";
        
        /* snip */
 
        private static readonly SilkIconResources _default = new SilkIconResources();
 
        /// <summary>
        /// Gets the default instance.
        /// </summary>
        /// <value>The default instance.</value>
        public static IIconResources Default
        {
            get { return _default; }
        }
 
        #region IIconResources Members
 
        public string Add
        {
            get { return _Add; }
        }
        
        public string Calendar
        {
            get { return _Calendar; }
        }
 
        public string Check
        {
            get { return _Check; }
        }
        
        /* snip */
 
        #endregion
    }

The next step is adding the WebResourceAttributes.  ContentType is a struct with string constants that contain the valid web resource content types, such as "image/png".

[assembly: WebResource(SilkIconResources._Edit, ContentType.Png)]
[assembly: WebResource(SilkIconResources._Delete, ContentType.Png)]
[assembly: WebResource(SilkIconResources._Add, ContentType.Png)]
[assembly: WebResource(SilkIconResources._Calendar, ContentType.Png)]
        /* snip */

The final step is implementing ResourceWebIconProvider, which is only a few lines of code.

    /// <summary>
    /// A <see cref="WebIconProvider"/> for Silk Icons (http://www.famfamfam.com/lab/icons/silk/).
    /// </summary>
    public class SilkIconProvider : ResourceWebIconProvider
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="SilkIconProvider"/> class.
        /// </summary>
        public SilkIconProvider()
            : base(SilkIconResources.Default, "Mark James", "http://www.famfamfam.com/lab/icons/silk/")
        {
        }
    }

WebControls

I also created a few WebControls to make displaying the icons easier.

Control Classes

WebIconImage is a control that inherits Image and adds a couple WebIconProvider related properties. You can use it just like an <asp:Image> except that you can specify the icon you want to display and it uses a WebIconProvider to determine the url.  The following markup would display the default WebIconProvider's help image.

<webicons:WebIconImage id="WebIconImage1" icon="Help" runat="server" />

 

WebIconCredit is control that displays the name of the icon set creator and a link to the creator's web site.  The name and link come from the configuration settings.  This control's markup is very simple.

<webIcons:WebIconCredit ID="WebIconCredit1" runat="server" />

 

Configuration

The configuration looks a lot like any provider configuration.

    <webIcons defaultProvider="Silk">
        <providers>
            <add name="Sanscons"
                 type="Sanscons.SansconsIconProvider, Sanscons"
                 cssClass="icon"/>
            <add name="Silk"
                 type="SilkIcons.SilkIconProvider, SilkIcons"/>
        </providers>
    </webIcons>

 

Source Code

The following zip file contains four WebIconProvider implementations and a test web site.

WebIcons.zip (2.18 mb)

Tags: ,

asp.net

A SiteMapProvider for Static Web Sites

by jrummell 3. August 2007 20:34

The new navigation features of ASP.Net 2.0 are pretty cool. If you haven't seen them yet, check out ScottGu's blog for more information.

I've seen a few blog posts on SiteMapProvider implementations for dynamic web sites, but not a whole lot on providers for static web sites. Sure, you could use the default implementation and manually update the web.sitemap xml file, but what about large sites? In my opinion, its not worth the effort.

Here are my requirements for a static SiteMapProvider:

  • Must automagically update whenever pages are added/removed.
  • Must be able to only include specified file types.
  • Must be able to exclude specified directories under the application's virtual directory.

So I went searching and didn't find anything. The closest I found was a macro by K. Scott Allen that generates a web.sitemap file from a web project. A noble effort, but I needed a bit more. So I set off to implement my own provider. Using the SqlSiteMapProvider example as a reference, I had created my own StaticFileSiteMapProvider by lunch time.

The implementation is rather straighforword. It starts at the application path (~/) and recurses each of its sub directories. There is a FileExtensions property that defines the types of files to include (e.g. aspx, html) and there is also a DirExclusions property that defines the directory name patterns to exclude (e.g. bin, App_*). The DefaultDocuments property defines the default document names for a directory (e.g index, default).

Why do I need a DefaultDocuments property? I can answer that with another question. What happens when you've got a directory in your app that doesn't have an index page? Well, the provider will generate a link to that folder, but clicking on it will result in a Directory Listing Denied error (at least I hope you would have your site set up that way). In
BuildSiteMap(SiteMapNode parentNode, string directory), if the current node's directory doesn't have a default document page, then the node's url isn't set, ensuring that its not hyperlinked.

On to the code. I've included the main parts of the class below. For a full listing, use the link at the end of this post.


public override SiteMapNode BuildSiteMap()
{
lock (this)
{
if (isBuilt)
{
return root;
}
string physicalAppPath = HttpContext.Current.Server.MapPath("~/");
BuildSiteMap(null, physicalAppPath);
isBuilt = true;
return root;
}
}
/// <summary>
/// Recursive method to build the site map.
/// </summary>
/// <param name="parentNode">The parent node.</param>
/// <param name="directory">The directory.</param>
private void BuildSiteMap(SiteMapNode parentNode, string directory)
{
// create the current node
string url = GetUrlFromPhysicalPath(directory);
string title = parentNode == null ? "Home" : Path.GetFileName(directory);
SiteMapNode node = new SiteMapNode(this, url, url, title);
// set the root
if (parentNode == null)
{
root = node;
}
// add a node foreach file
string[] files = GetFiles(directory);
foreach (string file in files)
{
url = GetUrlFromPhysicalPath(file);
SiteMapNode fileNode = new SiteMapNode(this, url, url, Path.GetFileNameWithoutExtension(file));
AddNode(fileNode, node);
}
// unset the url if there isn't an index file in the directory
if (!Array.Exists(files, delegate(string match)
{
foreach (string index in DefaultDocuments.Split(','))
{
if (String.Compare(Path.GetFileNameWithoutExtension(match), index.Trim(),
StringComparison.OrdinalIgnoreCase) == 0)
{
return true;
}
}
return false;
}))
{
// Note: setting node.Url to null doesn't change the value, so I'm setting it to String.Empty, 
// which is its default value
node.Url = String.Empty;
}
// recurse sub directories
string[] directories = GetDirectories(directory);
foreach (string dir in directories)
{
BuildSiteMap(node, dir);
}
// only add the current node if it has children
// Note: node.HasChildren throws an InvalidOperationException, so I'm checking the 
// file and directory arrays instead
if (files.Length > 0  directories.Length > 0)
{
AddNode(node, parentNode);
}
}

web.config settings:

<siteMap defaultProvider="StaticFileSiteMapProvider">
<providers>
<add name="StaticFileSiteMapProvider" type="Providers.StaticFileSiteMapProvider"
fileExtensions="asp, htm"
defaultDocuments="index"
dirExclusions="bin, obj, Properties, App_*, DMS, old" />
</providers>
</siteMap>

StaticSiteMapProvider.cs (11.85 kb)

Tags: ,

asp.net

Converting DateTime to String and back

by jrummell 21. May 2007 11:32
I spent about 20 frustrating minutes the other day wondering why a sql query wasn't selecting the record I wanted. Everything looked right until I stepped through my code the 3rd time. Then I discovered my problem.

In a web form, I allow a user to select a row in a GridView that fires an event to populate a DetailsView using an ObjectDataSource. The select method of the ObjectDataSource takes two parameters, a DateTime and a string. Since the date is coming from the GridView, I was just using Convert.ToDateTime([date cell].ToString()).

I discovered that the DateTime displayed in the GridView was '5/21/2007 8:51:42 AM' while the DateTime in the database was '2007-05-21 08:51:42.153'. They're close, but not exactly the same. It's that missing fraction of a second that made my where clause incorrect.

So then I thought, "How can I successfully convert a DateTime to a string and back?" After a short pause it hit me, "Ticks". No, not the kind of ticks I'm afraid of getting when backpacking in woods, but DateTime.Ticks. I used a HiddenField to store the string representation of the selected DateTime in ticks, and then added an overloaded select method that takes a ticks (long) parameter. In the new method I simply construct a DateTime from the ticks and call the original select method.

So in summary, to convert from DateTime to string and back, use ticks.

Tags:

asp.net

Powered by BlogEngine.NET 1.5.0.7
Theme by Mads Kristensen | Modified by Mooglegiant

About the author

John is a .Net Web Developer for a manufacturing company in Ohio. In his free time he enjoys web development, the outdoors, and spending time with his wife.

Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer's view in  anyway.

© Copyright 2009

Ads By Google