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.
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.
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.
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.
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.
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.
Older
got pivot?
Newer
Adding Multithreading
Older
got pivot?
Newer
Adding Multithreading
browse with Pivot
Codility Nitrogenium Challenge
OS X Lock
HACT '13
Codility Challenges
Priority Queue
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)
Comments
Leave a Comment
Please register or login to leave a comment.