#.think.in
learn.create.enjoy

Revisiting Pygments in the browser with Silverlight, now with BackgroundWorker

February 20, 2010 17:18 by tarn

A couple of week ago I blogged about using Pygments to do live syntax highlighting in the browser using Silverlight.

A major problem with the sample was that it did the pygmentizing on the UI thread which caused most browsers to become unresponsive. Today I wanted to fix that by using the BackgroundWorker to do the pygmentizing in a background thread.

Firstly I refactored the pygmentizing into a method that didn't interact with the UI.

def pygmentize_text(self, text, language):
    # attempt to pygmentize input with current language 
    try:

        from pygments import highlight
        from pygments.lexers import get_lexer_by_name
        from pygments.formatters import HtmlFormatter

        lexer = get_lexer_by_name( language, stripall=True)
        formatter = HtmlFormatter(linenos=False, cssclass="source")
        markup = highlight(text, lexer, formatter)

        return markup

    except:

        return "Error Generating Markup"



I then added a method that could be passed into a DoWorkEventHandler. It gets it arguments as a tuple from the event arguments and then sets the event argument result with the marked up HTML. The lack of explicit typing and use of tuples is good example of how some python idioms can be used when working with the .NET framework.

def worker(self, sender, e):

    # do work off UI thread. 
    e.Result = self.pygmentize_text(e.Argument[0],e.Argument[1])


The required BackgroundWorker and DoWorkEventHandler can be simply imported from the System.ComponentModel namespace.

from System.ComponentModel import BackgroundWorker, DoWorkEventHandler


The BackgroundWorker can then be setup and started. Again it's syntactically nice how the tuple can be created and passed as a RunWorkerAsync parameter.

def start_pygmentize(self):

    # update application state
    self.input_changed = False        
    self.pygmentizing = True
    self.show_message("pygmentizing..")

    # get paremters
    input = self.input.GetProperty("value")
    language = self.language.value

    # setup background worker
    worker = BackgroundWorker()
    worker.DoWork += DoWorkEventHandler(self.worker)
    worker.RunWorkerCompleted += self.complete

    # start the worker
    worker.RunWorkerAsync( (input,language) )


The completed event handler is the responsible for taking the markup generated by the BackgroundWorker and updating the DOM. It also fires off another worker if the source has changed since the last worker started.

def complete(self, sender, e):

    if e.Error:

        # handle errors/exceptions in worker
        self.source.SetProperty("innerHTML",e.Error.Message)

    else:

        # show the result
        self.source.SetProperty("innerHTML",e.Result)

    if self.input_changed:

        # input has changed, starty pygmentize again
        self.start_pygmentize()

    else:

        # no work queued
        self.pygmentizing = False
        self.hide_message()



The update has made the sample much more responsive, however it appears downloading the Silverlight application is still causing some browers to become a little unresponsive which is annoying. I will be interested to find out if this effect can be mitigated.

The actually pygmentizing processing could possibly be made a little faster by reusing the BackgroundWorker and only doing the Pygment imports once but the responsiveness of the browser has improved the sample enormously.

Check out the updated demo here.


Tags: , ,
Categories:
Comments (0)

Pygments in the browser with Silverlight

January 31, 2010 23:40 by tarn

17-02-2010 I've updated the demo to use the BackgroundWorker and posted about the update

I decided it might be fun to try get Python Markdown and Pygments running in the browser to enhance a markdown preview experience by eliminating the server-side round trips and provide a more responsive preview.

I managed to get it working entirely in Python but I found the application size excessively large (almost 3mb) and, more annoyingly, it seems to block the entire browser when it initially loads the pygments module. I think there is some sort of silverlight background thread I should be using.

I think it would work better with MarkdownSharp as pure C# silverlight applications are a fair bit leaner in size and probably run a little quicker than dynamic language applications. But this was for fun and I prefer coding in Python when not working.

17-02-2010 I've since found the MarkdownSharp doesn't do syntax hightlighting

I ended up having to write so little Python code to get this working that I can include it all here, syntax highlighted with Pygments of course.

from System.Windows import Application
from System.Windows.Controls import UserControl
from System.Windows.Browser import HtmlPage
from System import EventHandler

class App:

    def __init__(self):

        # load relevent HTML DOM elements
        self.input = HtmlPage.Document.GetElementById("input")
        self.source = HtmlPage.Document.GetElementById("output")
        self.language = HtmlPage.Document.GetElementById("lang")

        # fire javascript functions to indicate the application has been load
        HtmlPage.Window.CreateInstance("silverlight_loaded");

        # pygmentize initial 
        self.pygmentize()

        # register events
        self.input.AttachEvent('onkeyup', EventHandler( self.update_handler )) 
        self.language.AttachEvent('onchange', EventHandler( self.update_handler ))

        # fire javascript function to indicated the pygments has been loaded
        HtmlPage.Window.CreateInstance("pygments_loaded");

   # handle language or input changes by pygmentizing 
    def update_handler(self, sender, e):

        self.pygmentize()

    def pygmentize(self):
        input = self.input.GetProperty("value")

        # attempt to pygmentize input with current language 
        try:

            from pygments import highlight
            from pygments.lexers import get_lexer_by_name
            from pygments.formatters import HtmlFormatter

            lexer = get_lexer_by_name(self.language.value, stripall=True)
            formatter = HtmlFormatter(linenos=False, cssclass="source")
            markup = highlight(input, lexer, formatter)

            # update the preview
            self.source.SetProperty("innerHTML",markup)

        except:

            # indicate there was an error in pygmentize
            self.source.SetProperty("innerHTML", "Error Generating Markup" )

# Do it!    
App()


Despite the fact that there isn't much code, the development experience writing silverlight application in python is a bit of a pain in the arse. Granted it's much better with the python console in the browser and better error reporting in most recent SDK, but it still sucks; debugging and logging support is very limited and on some errors the application dies without reporting anything.

Another difficulty is having to manually copy all the python standard library modules required by the module from the library folders into the application (which explains something about the bloated application size). And even though the code in the demo works, some very similar code from the pygments quick start doesn't.

Check out the live demo.


Tags: , ,
Categories:
Comments (0)

Silverlight Dynamic Languages SDK

January 26, 2009 15:12 by tarn

I decided to have a look at creating Silverlight applications with IronPython, this led me to the Silverlight Dynamic Languages SDK. The main piece of the SDK is Chiron which is a development utility for creating Silverlight application packages (XAPs) from dynamic languages.

Chiron can be used to:

  • Create dynamic language Silverlight projects
  • Start a local web server which dynamically generates XAPs from the project files
  • Write generated XAPs to disk.   

I needed something to build on Silverlight with IronPython, so I decided to write a small part of the UI for an RSS Feed Visualizer I've been wanting to write. Its only really a demo, the UI could use some more work and I've just used some sample names as data. I hope to build on this example in the future, but I'll need to do some refactoring, the code is pretty scrappy as I wrote most of it after I lost a house poker game. Apparently a stronger understanding of counter intuitive probability problems hasn't helped my game and I don't write my best code after an afternoon of drinking. 

(If there is problems viewing the demo on this page, it can also be viewed here)

Here is the IronPython code that drives the demo, I've also uploaded the entire project. I used examples of dragging and inertia from the excellent Project "Rosetta Stone" Tutorials.

   1: from System.Windows import Application
   2: from System.Windows.Controls import UserControl
   3: from System.Windows.Controls import TextBlock
   4: from System.Windows.Controls import Canvas
   5: from System.Windows.Media import Colors
   6: from System.Windows.Media import SolidColorBrush
   7: from System.Windows import Visibility
   8: from System.Windows import FontWeights
   9: from System.Windows import HorizontalAlignment
  10: from System.Windows.Media.Animation import Storyboard
  11: from System import Random;
  12: import math
  13:  
  14: class App:
  15:   def __init__(self):
  16:     self.root = Application.Current.LoadRootVisual(UserControl(), "app.xaml");
  17:     spinner = Spinner(self.root,['Tarn','Leah','Alex','Mick','John','Rex','Jack','Jill','Elle']);
  18:  
  19:  
  20: class Spinner():
  21:   def __init__(self, root, names):
  22:     self.root = root;
  23:     self.names = names
  24:     self.radius = 0;
  25:     self.textBlocks = [];
  26:     self.mainText = Block(self.root.layout_root,self.centreClick);
  27:     self.mainText.SetPosition(100.0,100.0);
  28:     self.mainText.HorizontalAlignment = HorizontalAlignment.Center;
  29:     self.selectedItem = None;
  30:     self.angle = 0;
  31:     self.diff = 0;
  32:     self.target = 0;
  33:     self.drag = False;
  34:     self.createTextBlocks(self.names[0], self.names);
  35:     self.root.MouseLeftButtonDown += self.leftButtonDown;
  36:     self.root.MouseLeftButtonUp += self.leftButtonUp;
  37:     self.root.MouseMove += self.mouseMove;
  38:     self.sb = Storyboard();
  39:     self.root.Resources.Add("sb",self.sb);
  40:     self.sb.Completed += self.enterFrame;
  41:     self.sb.Begin();
  42:   def centreClick(self,item):
  43:     self.selectedItem = item;
  44:     self.state = "Selected";
  45:     pass;
  46:   def createTextBlocks(self, item, list):
  47:     self.state = "Expanding";
  48:     self.mainText.text.Opacity = 1;
  49:     self.mainText.text.Text = item;
  50:     self.velocity = 0.01;
  51:     for textBlock in self.textBlocks:
  52:       textBlock.dispose();
  53:     textBlocks = [];
  54:     i = 0;
  55:     list = filter(lambda m: (m != item), list)
  56:  
  57:     for title in list:
  58:       textBox = Block(self.root.layout_root,self.centreClick);
  59:       textBox.text.Text = title;
  60:       textBox.angle = i * 2 * math.pi / list.__len__();
  61:       textBox.HorizontalAlignment = HorizontalAlignment.Center;
  62:       textBox.radius = -(100/list.__len__()) * i;
  63:       textBox.SetPosition2(0);
  64:       self.textBlocks.append(textBox);
  65:       i += 1;
  66:   def enterFrame(self, object, e):
  67:     diff = ((self.target - self.diff)-self.angle) ;
  68:     if (self.drag == True):
  69:       oldAngle = self.angle;
  70:       if (diff > math.pi): diff = diff - math.pi * 2;
  71:       if (diff <= -math.pi): diff = diff + math.pi * 2;     
  72:       self.angle  = self.angle + ((diff) * 0.2); 
  73:       self.velocity = self.angle - oldAngle;
  74:     else:
  75:       self.angle = self.angle + self.velocity;
  76:       if (math.fabs(self.velocity) > 0.01): 
  77:         self.velocity = self.velocity * 0.90;
  78:     self.angle = self.angle % (2 * math.pi);
  79:  
  80:     if (self.state == "Expanding"):
  81:       expandComplete = True;
  82:       for textBlock in self.textBlocks:
  83:         if (textBlock.Expand(100) == False): expandComplete = False;    
  84:     if (self.state == "Selected"):
  85:       expandComplete = True;
  86:       if (self.selectedItem != self.mainText): 
  87:         self.mainText.text.Opacity -= 0.10;
  88:         self.selectedItem.moveTo(100,100);
  89:       for textBlock in self.textBlocks: 
  90:         if (self.selectedItem != textBlock):
  91:           textBlock.text.Opacity -= 0.010;
  92:           if (textBlock.Expand(200) == False): expandComplete = False;    
  93:       if (expandComplete == True): 
  94:         self.createTextBlocks(self.selectedItem.text.Text, self.names);
  95:     for textBlock in self.textBlocks:
  96:       if (self.selectedItem != textBlock): textBlock.SetPosition2(self.angle);
  97:     self.sb.Begin();
  98:   def leftButtonUp(self,object,e):
  99:     self.root.ReleaseMouseCapture();
 100:     self.drag = False;
 101:   def mouseMove(self, object, e):
 102:     if (self.drag == True):
 103:         self.target = self.getAngle(e);
 104:   def leftButtonDown(self, object, e):
 105:       self.root.CaptureMouse();
 106:       angle = self.getAngle(e);
 107:       self.diff = angle - self.angle;
 108:       self.target = self.getAngle(e);
 109:       self.drag = True;
 110:   def getAngle(self, e):
 111:     x = e.GetPosition(self.root).X-100;
 112:     y = e.GetPosition(self.root).Y-100;
 113:     angle = (math.atan2(x,y));
 114:     if (angle < 0): angle = angle + (math.pi*2);
 115:     return angle;
 116:  
 117: class Block():
 118:   def __init__(self, canvas, clicked):
 119:     self.clicked = clicked;
 120:     self.canvas = canvas;
 121:     self.text = TextBlock();
 122:     self.text.Foreground = SolidColorBrush(Colors.White);
 123:     self.offset = 100;
 124:     self.radius = 10;
 125:     self.angle = 0;
 126:     canvas.Children.Add(self.text);
 127:     self.text.MouseEnter += self.mouseOver;
 128:     self.text.MouseLeave += self.mouseOut;
 129:     self.text.MouseLeftButtonDown += self.mouseClick;
 130:   def dispose(self):
 131:     self.canvas.Children.Remove(self.text);
 132:   def moveTo(self, x, y):
 133:     self.text.FontWeight = FontWeights.Normal;
 134:     currentX = self.text.GetValue(Canvas.LeftProperty);
 135:     currentY = self.text.GetValue(Canvas.TopProperty);
 136:     newX = currentX + ( (x - currentX) * 0.1);
 137:     newY = currentY + ( (y - currentY) * 0.1);
 138:     self.SetPosition(newX,newY);
 139:   def Expand(self, max):
 140:     if (self.radius < max):
 141:       self.radius += 1;
 142:       return False;
 143:   def mouseClick(self,sender,e):
 144:     self.clicked(self);
 145:   def mouseOver(self,sender,e):
 146:     self.text.FontWeight = FontWeights.Bold;
 147:   def mouseOut(self,sender,e):
 148:     self.text.FontWeight = FontWeights.Normal;
 149:   def SetPosition(self, x, y):
 150:     self.text.SetValue(Canvas.LeftProperty, x);
 151:     self.text.SetValue(Canvas.TopProperty, y);    
 152:   def SetPosition2(self, angle):
 153:     if (self.radius < 20):self.text.Visibility = Visibility.Collapsed;
 154:     else:self.text.Visibility=Visibility.Visible;
 155:     self.SetPosition( (math.sin(self.angle + angle) * self.radius) + self.offset, (math.cos(self.angle + angle) * self.radius) + self.offset);  
 156:   
 157:  
 158: App()

I noticed when I uploaded the XAP that it was quite large (> 1Mb) for a very small demo. I think this is because additional assemblies for dynamic languages and IronPython are not included in the client Silverlight plug-in and have to be included in the XAP.


Tags: , ,
Categories:
Comments (2)

Desktop Racer

December 16, 2008 12:10 by tarn

I've finally got around to publicly hosting my DevSta entry - Desktop Racer. Turns out it was pretty easy, I was just too burnt out by it to want to do anything with it till now.

Play Desktop Racer Now

Looking at it now, I'm a little disappointed I didn't just go a little bit further and add a two player mode as I think it would  have actually made it fun to play. But honestly after the sixth day of the competition I was so over it I didn't care.

I would like to add the two player mode and some performance improvements to make it a bit more fun to play, but I seriously doubt I'll ever get round to doing it. I'm already half way into an ASP.NET MVC weblog project and I've also suddenly become really interested in learning Python.

I'm going to upload the source as well for anyone who is interested, but I warn you its probably not a great example Silverlight application as I was just learning what was a beta technology at the time. As the competition was only 200 hours I didn't spend much time pondering what would be the best way to implement it, I pretty much just got in and started coding. Some logic is nicely packaged but some classes just ended up getting way to large and unwieldy. I've since seen some good demos and blogs on patterns to help separate out the presentation and logic.

There are some cool-ish parts in the code; I like how the level and all the level elements are defined in XAML so you can visualize the level at design time in Visual Studios. I also kind of like how the all the level elements the car can collide with implement an interface that has its geometric information.  This allowed my to simplify the collision logic to two type of geometric primitive and basically calculate and collision with all the level elements in a single loop.


Tags: , ,
Categories: Demos
Comments (0)

#.think.in infoDose #4 (20th Oct - 25th Oct)

October 27, 2008 09:22 by brodie

Notable Events

Silverlight

Architecture

ASP.NET AJAX

ASP.NET MVC

jQuery

Testing

Usability

Developer Skills

Singularity Watch

Other

Tips


#.think.in infoDose #3 (13th Oct - 17th Oct)

October 20, 2008 08:15 by brodie

Notable Events

Management/Process

Architecture

ASP.NET

Silverlight

Other

Singularity Watch

Books to Read

Quotes

  • "An engineer will endlessly tinker with a project as long as it's sitting in front of them. Endlessly" - rands (twitter)
  • "There ain’t no rules around here. We’re trying to accomplish something." —Thomas Edison