Wednesday, 27 July 2011

Creating a XAML Rubber Stamp

This post illustrates how to create a rubber stamp to use in WPF applications.  We'll first create a textured background in Inkscape do draw our 'stamp' with.  We'll then save the texture as a PNG and then use that image as an ImageBrush to paint a rectangle border with some wording within.  We'll give the border a bit of rounding and then finish it off with a RotateTransform to make it look genuine.  We'll end up with this:

Prerequisites:
  1. Inkscape
  2. MSPaint
  3. Visual Studio

 First, we'll create the background (you can skip this section and just download StampBrush.png):
  1. Open Inkscape.
  2. Key F4 to select the Rectangle tool and create a rectangle roughly the same size as the document.
  3. Bring up the Fill and Stroke window (Ctrl-Shift-F).
  4. Under the 'Fill' tab, click the 'Flat Color' button and enter a value of 170 for R.
  5. File -> Properties -> Resize page to Content.
  6. Key F1 to select the Selector tool, and select the rectangle drawing.
  7. Ensure the drawing is zoomed to 100% (bottom right corner of screen).
  8. Filters -> Overlays -> Rubber Stamp.
  9. Take a screenshot (Alt-PrntScrn).
  10. Open MSPaint, and paste the screenshot onto the canvas.
  11. Move and crop the image so that only the textured effect is visible on the canvas.
  12. Save the image as StampBrush.png.
Now that we have the background, we'll create the border and text in WPF:
  1. Open Visual Studio and create a new WPF project.
  2. Copy StampBrush.png into your project.
  3. Open MainWindow.xaml, and copy the following attribute into the Window element:
xmlns:system="clr-namespace:System;assembly=mscorlib"
  1. replace the Grid element with the below XAML:
    <Viewbox>
        <Canvas Width="250" Height="320">
            <Canvas.Resources>
                <ImageBrush x:Key="stampBrush" ImageSource="StampBrush.png" Stretch="None" />
                <system:String x:Key="StampText">SAMPLE</system:String>
            </Canvas.Resources>
            <WrapPanel Canvas.Top="0" Canvas.Left="70">
                <WrapPanel.RenderTransform>
                    <RotateTransform CenterX="0" CenterY="0" Angle="45"></RotateTransform>
                </WrapPanel.RenderTransform>
                <Border BorderBrush="{StaticResource stampBrush}" BorderThickness="6" Margin="10" Padding="10,0,10,0" CornerRadius="3">
                    <TextBlock Foreground="{StaticResource ResourceKey=stampBrush}" FontSize="72" FontWeight="Bold" Text="{StaticResource StampText}"></TextBlock>
                </Border>
            </WrapPanel>
        </Canvas>
    </Viewbox>

That's it!  Hit F5 and you'll have a window with a stamp in it.  From here you can copy the Viewbox into a UserControl or make it a resource for use in your apps.

If you wish, you can reduce the size of the bitmap by opening StampBrush.png in MSPaint and cropping it down to the actual size of your stamp.

Of course, you can skip all the above steps and just download the demo project.

Thursday, 7 July 2011

Using the Google openid-selector with ASP.NET

Today we'll walk through a very simple implementation of the Google openid-selector in ASP.NET.  When we're finished we'll have a login page that looks like this:


Prerequisites

  1. An existing ASP.NET website project
  2. Basic understanding of ASP.NET
  3. Basic c# programming skills

Procedure

  1. Download DotNetOpenAuth  (I'm using v3.4.7).  Locate DotNetOpenAuth.dll and add it as a reference to your ASP.NET project.
  2. Download openid-selector (I'm using v1.3). Extract the contents, and copy the openid-selector/Images folder to the root of your website.  Copy the openid-selector folder to the root of your website.  The folder structure should look like this:

  3. Create a new page named Login.aspx, with the following markup:

    <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Login.aspx.cs" Inherits="OpenIdSelectorDemo.Default" %>
    
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head runat="server">
        <title></title>
        <link type="text/css" rel="stylesheet" href="openid-selector/css/openid.css" />
        <script type="text/javascript" src="openid-selector/js/jquery-1.2.6.min.js"></script>
        <script type="text/javascript" src="openid-selector/js/openid-jquery.js"></script>
        <script type="text/javascript" src="openid-selector/js/openid-en.js"></script>
        <script type="text/javascript">
            $(document).ready(function () {
                openid.init('openid_identifier');
            });  
        </script>
    </head>
    <body>
        <form id="openid_form" name="action" action="Login.aspx" method="post">
        <div>
            <fieldset>
                <legend>Sign-in or Create New Account</legend>
                <div id="openid_choice">
                    <p>
                        Please click your account provider:</p>
                    <div id="openid_btns">
                    </div>
                </div>
                <div id="openid_input_area">
                    <input id="openid_identifier" name="openid_identifier" type="text" value="http://" />
                    <input id="openid_submit" type="submit" value="Sign-In" />
                </div>
            </fieldset>
        </div>
        </form>
    </body>
    </html>
    
    


  4. Open Login.aspx.cs, and place in it the following code:

        public partial class Default : System.Web.UI.Page
        {
            private static OpenIdRelyingParty openid = new OpenIdRelyingParty();
            protected void Page_Load(object sender, EventArgs e)
            {
                var response = openid.GetResponse();
                if (response == null)
                {
                    Identifier id;
                    if (Identifier.TryParse(Request.Form["openid_identifier"], out id))
                    {
                        try
                        {
                            IAuthenticationRequest request = openid.CreateRequest(Request.Form["openid_identifier"]);
    
                            ClaimsRequest fields = new ClaimsRequest();
                            fields.Email = DemandLevel.Request;
                            request.AddExtension(fields);
    
                            request.RedirectToProvider();
                        }
                        catch (ProtocolException ex)
                        {
                            writeError(ex.Message);
                        }
                    }
                    return;
                }
    
                switch (response.Status)
                {
                    case AuthenticationStatus.Authenticated:
                        UriIdentifier id = (UriIdentifier)response.ClaimedIdentifier;
                        ClaimsResponse claimsResponse = response.GetExtension<ClaimsResponse>();
                        string userName = response.ClaimedIdentifier.ToString();
                        loginUser(userName, claimsResponse);
                        break;
                    case AuthenticationStatus.Canceled:
                        writeError("Canceled at provider");
                        break;
                    case AuthenticationStatus.Failed:
                        writeError(response.Exception.Message);
                        break;
                }
    
            }
    
            private void writeError(string message)
            {
                Response.Write(message);
            }
    
            private void loginUser(string userName, ClaimsResponse claimsResponse)
            {
                Response.Write(userName);
                //User has been verified by their OpenID provider.  Add code here to interact with
                //your membership provider and redirect to your website.
            }
        }
    

  5. From here you can edit the loginUser(...) method to fit in with your projects login process.

You can download my demo project here.

Tuesday, 5 July 2011

ReCAPTCHA for DotNetNuke Forums

This post walks through implementing Googles reCAPTCHA control into a DotNetNuke Forums web site.  It seems like it would be pretty straightforward, but there are some hurdles along the way.

These instructions are specific for DNN Forums, but you can use the same techniques for other DNN modules.

Prerequisites

  1. A DNN website with the DNN Forum module installed.
  2. The DNN Forum source code (I'm working with release 5.00.01).
  3. Basic VB.NET programming skills.
  4. Basic ASP.NET skills.


The first step (the easy part), is to edit the post page so that it contains the reCAPTCHA control.
  1. Visit the reCAPTCHA website, create an account and generate the requisite keys.
  2. Download recaptcha.dll and place it in your sites bin directory.
  3. Add the following line to the top of the Forum_PostEdit.ascx:
    <%@ Register TagPrefix="recaptcha" Namespace="Recaptcha" Assembly="Recaptcha" %>
    
  1. In Forum_PostEdit.ascx, locate the <tr id="rowModerate" ...> element, and immediately after, insert a new <tr> containing the recaptcha control:
    <tr>
        <td align="center" width="100%">
            <recaptcha:RecaptchaControl ID="recaptcha1" runat="server" Theme="red" 
            PublicKey="[YourKey]"
            PrivateKey="[YourKey]" />
        </td>
    </tr>
    

Now we will add logic so that the post does not get submitted if the reCAPTCHA is not complete.
  1. Open the DotNetNuke.Forum project in Visual Studio.
  2. Add a reference to recaptcha.dll.
  3. In Forum_PostEdit.ascx.vb, add the following line somewhere to the class body:
    Protected WithEvents recaptcha1 As Recaptcha.RecaptchaControl
    
  1. Find cmdSubmit_Click(...), and add the following code to the start of the method:
    If Not recaptcha1.IsValid Then
        Return
    End If
    
At this point, the reCaptcha will appear on the page, and will prevent the user from proceeding to submit the post until they have completed the CAPTCHA correctly.
The problem is that the user only gets one try - after the first attempt, the reCAPTCHA control will disappear.
This is a side effect of the UpdatePanel used in DNN to prevent the entire page from being posted when the user clicks post.
To work around this problem, we will now add a PostUpdateTrigger to the UpdatePanel to force a full PostBack when the user clicks Submit.
  1. Add the following two functions to Forum_PostEdit.ascx.vb:
    Private Function findUpdatePanelParent(uc As Control) As UpdatePanel
        If TypeOf uc Is UpdatePanel Then
            Return DirectCast(uc, UpdatePanel)
        End If
        Return findUpdatePanelParent(uc.Parent)
    End Function
    
    Private Sub registerFullPostback()
        Dim updatePanel As UpdatePanel = findUpdatePanelParent(Me)
        Dim trig As PostBackTrigger = New PostBackTrigger()
        trig.ControlID = "Forum_PostEdit$cmdSubmit"
        updatePanel.Triggers.Add(trig)
    End Sub
    
    

  1. Locate Page_Init(...), and add the following line:
    registerFullPostback()
    
We're nearly done. All you need to do now is recompile DotNetNuke.Forum, and copy DotNetNuke.Forum.dll to your websites bin directory.

Sunday, 20 March 2011

Lazy loading with Details View

Today I started testing an application for keyboard navigability, and found a pretty serious problem when using arrow keys to move around a ListBox.

To simplify the design, I have a ListBox alongside a Details View showing the details for the SelectedItem.  When an item is selected, a web service call is made on a separate thread to populate the details for that item.

This has been working well, but when using the keyboard arrows to select an item towards the bottom of the list, things start to get nasty.  As each item in the list gets temporarily selected as I move towards the item I actually want to select, those items all have their details loaded.

This behavior causes a bunch of unwanted web service calls to be made (a needless burden on the server), as well as a large number of threads to be consumed simultaneously on the client (causing thread starvation), such that the details for the actual item we want take much (much) longer to load. It can take minutes for the application to return to a usable state!

To solve this problem, I needed to introduce some new behavior to the loading of the selected item:
  1. When an item is selected:
    • create a new Task but wait 500ms before proceeding to make the web service call;
    • cancel any previous Tasks which are still waiting to proceed with the web service call.
  1. Once the 500ms has expired, if the Task has not been canceled, assume the selection is intended and go ahead with the web service call.
This will add a 500ms overhead to the loading of details, but I can't think of a way to avoid this without reading the users mind.


To implement this behavior, I created a new class named LagLoader.  To use it, each selection scope (ie List) should have one LagLoader.  LagLoader is initialized with an Action which should be called to load the selected item. LagLoader.LoadItem(SelectedItem) should be called whenever an item is selected.  After half a second, if no other item has been selected, it will go ahead and call the loader function.


/// <typeparam name="T">The type of object which will be selected.</typeparam>
public class LagLoader<T>
{
    /// <param name="loader">The function which should be called when an item is selected.</param>
    public LagLoader(Action<T> loader)
    {
        _loader = loader;
    }
 
    private Action<T> _loader;
 
    private AutoResetEvent _waitHandle = new AutoResetEvent(false);
    private T _selectedItem;
    public void LoadItem(T item)
    {
        _waitHandle.Set(); //interrupt any Tasks which are waiting.
        _waitHandle = new AutoResetEvent(false);
        _selectedItem = item;
 
        Task.Factory.StartNew(() =>
        {
            if (_waitHandle.WaitOne(500))
                return//Another selection interrupted the wait.
            _loader(item);
        });
    }
}
I'm pretty happy with the way this worked out, but if you know of a better way I'd love to hear about it.

Thursday, 17 March 2011

iAmpRemote - bridging the gap

For a few months I’ve been playing tv shows on my laptop and viewing them on the TV.  This has worked well, but it’s a bit messy – there are cables running everywhere and it’s a hassle to remove/replace the plugs whenever I want to use my laptop for something else.  Time for a revamp.

The idea was to have a low profile desktop computer sitting on a shelf under the TV.  The computer would have output to the TV and LAN connectivity, but no mouse or keyboard.  There would be a network share which I could drop movies into from my office computer, and I would control Winamp remotely on my iPhone using iAmpRemote.  Winamp would automatically detect any new files and add them to the Media Library.

I expected this to be a pretty straight-forward setup – the tools are readily available and the design is pretty simple.

After setting up the computer and configuring the OS, the last remaining task was to start using iAmpRemote – I have not used this app before, but my needs are pretty simple and it looks like a respectable application, so I expected things would go pretty smoothly.  I was bitterly disappointed to discover that the only way to browse my files with iAmpRemote was using the Media Library Playlists (the iAmpRemote website touts the ability to “control your playlist”, which is just plain wrong unless by “control” you mean “view and select from”).  Winamp does scan my computer for new media files and organizes them into various categories (audio, video, never played etc), but I could not find a way to make Winamp put these files into playlists (and, apart from iAmpRemotes inability to browse the Media Library, there would be no reason for it to do this).

I was unable to find a good alternative to iAmpRemote, and I was also unable to find a tool to make it work well, so I ended up (sigh) writing a console app.

The app is designed to run on the host machine before Winamp is opened (I have it running from a batch file at startup).  It scans a directory (configurable), finds all media files within each subdirectory and puts them into a playlist.  It then saves each playlist into the Media Library AppData folder, and writes an entry for each generated playlist into playlists.xml (everwriting the previous copy of playlists.xml).  This causes each subfolder to appear as a playlist in the Winamp Media Library, which in turn allows iAmpRemote to view my files.

I’m making the program and it’s code (c#, VS2010) publicly available for anyone who needs it - you can find it here.

Playlist Generator for Winamp

This app is designed to make iAmpRemote easier to use by generating playlists for your media files, based on the directory structure they are in.

It scans a directory (configurable), finds all media files within each subdirectory and puts them into a playlist.  It then saves each playlist into the Media Library AppData folder, and writes an entry for each generated playlist into playlists.xml (everwriting the previous copy of playlists.xml).  This causes each subfolder to appear as a playlist in the Winamp Media Library, which in turn allows iAmpRemote to view my files.

You can download the source (c#, VS2010), or just grab the executable.

Playlist Generator Instructions
1.       Download and unzip PlaylistGenerator.zip.
2.       Edit config.xml:
a.       Set ScanDirectory.path to your root media folder
b.      Set MediaLibrary.path to your Media Library Plugin folder ("C:\Users\[UserName]\AppData\Roaming\Winamp\Plugins\ml")
3.       Close winamp if it is open.
4.       Run PlaylistGenerator.exe.
5.       Open Winamp.

*Be aware that any existing entries in your Media Library Playlists will be removed.
Troubleshooting
1.       The .NET Framework 3.5 is required.
2.       If an error occurs, it will be written to the console.  If the cause is not clear you might need to refer to the code, or post a question in the comments and I will try to help.
3.       The first time you open the playlist library in iAmpRemote, it may give an “Empty Playlist Library” error.  Simply restart Winamp if this happens.