Register | Login

Stacking Code

public interface IBlog { string Dump(Stream consciousness); }

Screenshot Generator

Saturday, 1 January, 2011 @ 8:55 AM < Adam Boddington
Tags: Architecture, Building Neno, NHibernate, Pivot, Windows Forms

This is post #31 in the Building Neno series. Please click here for a description of the Building Neno project and instructions on how to access the source code for this post.

Time to automate the generation of screenshots for my Pivot collection. I manually captured 30 posts for the initial collection and I really don't want to do that again if I can help it. Googling for C# screenshot code brings back a lot of examples using the System.Windows.Forms.WebBrowser control. I've not used it before, and I was really hoping for a solution using Firefox, but I couldn't find much else.

Firing up a quick instance shows it renders my site just fine. It appears to use the installed version of Internet Explorer to do the actual rendering -- which is great, because I know my site renders okay in IE, and a managed code solution from end-to-end will really make generating screenshots easy.

WebBrowser couldn't render the embedded Flash video in my last post. I realised I hadn't installed Flash for IE yet.

Second UI

I tried instantiating the WebBrowser control in a console application with the intent of moving the code to a controller in my MVC web application. However, it can't be instantiated in a non-STA thread -- and that's really more than I want to figure out just now. So the alternative is a quick and dirty Windows Forms application. (There's no guarantee my hosted server will have IE with JavaScript enabled or Flash installed anyway.)

Whether I like it or not, I now have my second UI for my application and I need to think about how it will talk to the application itself. The web application has the luxury of consuming my service layer assemblies directly. If I do the same thing in my Windows application, I will have more than one instance of my application logic running -- and that makes managing change an issue. Click-once deployment can make the upgrade process easy, but it doesn't automatically solve the issue of already running instances. The best solution is probably to build a web API for the Windows application to work with. But it's an issue I can solve for the next UI -- for this quick and dirty administrator-only Windows application, even click-once deployment is overkill. I just need to remember to recompile the Windows application whenever I change anything in the service layer or below -- or I risk having an old version of application logic harming my data integrity.

How Dirty is Dirty?

I don't do a lot of Windows Forms or WPF programming and it shows in this code. Everything is running in the main thread, making the UI unresponsive during execution. I'll attempt to address that later on.

The plan is to have four WebBrowser controls to try to alleviate load latency as much as possible and always have something for the application to do. I'll throw all the posts that need screenshots into a queue. The WebBrowser controls can dequeue a new post whenever they finish the one they're on.

Windows Applications and NHibernate

I'm not an expert, but when working with NHibernate in a Windows application, using a single NHibernate session for the entire life of the application works for me. Occasionally evicting objects or clearing the session is required to keep memory usage down. Configuring the IOC container to return singletons is straight forward. The main gotcha in a multi-threaded environment is to make sure only one operation is running through the NHibernate session at a time or the underlying command objects can get garbled. Using lock on an appropriate object (like the NHibernate session itself) will avoid that situation.

Edit: This goes against conventional wisdom which opts for one NHibernate session per thread instead. In this application (at the moment) it's the same thing -- but preparing for one session per thread can avoid a lot of issues further down the line.

Bad Form

Application settings, IOC configuration, and instantiation of the four WebBroswer controls is all done when the form loads.

WebBrowser1 = new WebBrowser { Height = 1024, Name = "WebBrowser1", ScrollBarsEnabled = false, Width = 1024 };
WebBrowser1.DocumentCompleted += WebBrowser1_DocumentCompleted;

Each WebBrowser control is given something to do when their page is done loading.

private void WebBrowser1_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)
{
    CaptureScreenshot(WebBrowser1, e.Url.ToString());
    NavigateToNextPost(WebBrowser1);
}

Hitting the "Go" button starts the screenshot generation. Even though everything is running in the same thread, I'm still thinking ahead to a possible multithread refactor. I'm locking centrally managed resources -- and not doing a very good job of it either.

private void GoButton_Click(object sender, EventArgs e)
{
    // ...

    // Posts
    lock (Moja.InversionOfControl.Container.Get<ISession>())
        PostQueue = new Queue<Post>(Moja.InversionOfControl.Container.Get<IPostService>().GetPosts().Where(post => post.IsPublished));

    AddMessage("{0} posts queued.", PostQueue.Count);

    if (PostQueue.Count == 0)
        return;

    // Queue them up.
    NavigateToNextPost(WebBrowser1);
    NavigateToNextPost(WebBrowser2);
    NavigateToNextPost(WebBrowser3);
    NavigateToNextPost(WebBrowser4);
}

The NavigateToNextPost method handles the dequeuing of a post and the navigation to the right URL.

private void NavigateToNextPost(WebBrowser webBrowser)
{
    Post post = null;

    lock (PostQueue)
        if (PostQueue.Count > 0)
            post = PostQueue.Dequeue();

    lock (WebBrowserPostHash)
        WebBrowserPostHash[webBrowser.Name] = post;

    if (post != null)
    {
        string url = BaseUrl + string.Format("blog/{0}/{1:00}/{2:00}/{3}", post.PublishDate.Year, post.PublishDate.Month, post.PublishDate.Day, post.Slug);
        webBrowser.Navigate(url);
        AddMessage("{0} navigating to {1}.", webBrowser.Name, url);
    }

    // ...
}

The CaptureScreenshot method is called when the page is loaded and handles generating the screenshot and the uploading of the screenshot to the application.

private void CaptureScreenshot(WebBrowser webBrowser, string url)
{
    AddMessage("{0} loaded {1}.", webBrowser.Name, url);
    var bitmap = new Bitmap(1024, 1024);
    var targetBounds = new Rectangle(0, 0, 1024, 1024);
    webBrowser.DrawToBitmap(bitmap, targetBounds);

    Post post = WebBrowserPostHash[webBrowser.Name];

    // Save to file.
    //var path = "C:\\Users\\Adam Boddington\\Desktop\\Screenshots\\" + url.Replace(":", "-").Replace("/", "-").Sluggify() + ".png";
    //bitmap.Save(path, ImageFormat.Png);
    //AddMessage("{0} saved screenshot to {1}", webBrowser.Name, path);

    // Save to post.

    var memoryStream = new MemoryStream();
    bitmap.Save(memoryStream, ImageFormat.Png);

    Attachment attachment = post.Attachments.Where(a => a.Slug == "pivot-screenshot").SingleOrDefault();

    if (attachment == null)
    {
        attachment = new Attachment();
        post.Attachments.Add(attachment);
    }

    attachment.Name = "Pivot Screenshot.png";
    attachment.Slug = "pivot-screenshot";
    attachment.ContentType = "image/png";
    attachment.Store(memoryStream.ToArray());

    lock (Moja.InversionOfControl.Container.Get<ISession>())
        Moja.InversionOfControl.Container.Get<IPostService>().UpdatePost(post);

    AddMessage("{0} uploaded screenshot for {1}.", webBrowser.Name, url);
}

The result is a slow, clunky, unresponsive Windows application.

Screenshot Generator

I'm not happy with it but at least it's better than doing it manually. It's still going to bug me until I refactor it though.

There are 0 comments.


Comments

Leave a Comment

Please register or login to leave a comment.


Older
got pivot?

Newer
Adding Multithreading

Older
got pivot?

Newer
Adding Multithreading

browse with Pivot


About


Projects

Building Neno


RSS
Recent Posts

Codility Nitrogenium Challenge
OS X Lock
HACT '13
Codility Challenges
Priority Queue


Tags

Architecture (13)
ASP.NET (2)
ASP.NET MVC (13)
Brisbane Flood (1)
Building Neno (38)
C# (4)
Challenges (3)
Collections (1)
Communicator (1)
Concurrency Control (2)
Configuration (1)
CSS (5)
DataAnnotations (2)
Database (1)
DotNetOpenAuth (2)
Entity Framework (1)
FluentNHibernate (2)
Inversion of Control (5)
JavaScript (1)
jQuery (4)
Kata (2)
Linq (7)
Markdown (4)
Mercurial (5)
NHibernate (20)
Ninject (2)
OpenID (3)
OS X (1)
Pivot (6)
PowerShell (8)
Prettify (2)
RSS (1)
Spring (3)
SQL Server (5)
T-SQL (2)
Validation (2)
Vim (1)
Visual Studio (2)
Windows Forms (3)
Windows Service (1)


Archives


Powered by Neno, ASP.NET MVC, NHibernate, and small furry mammals. Copyright 2010 - 2011 Adam Boddington.
Version 1.0 Alpha (d9e7e4b68c07), Build Date Sunday, 30 January, 2011 @ 11:37 AM