Welcome to MSDN Blogs Sign in | Join | Help

BeginAnimation() method for Silverlight

Yeah, it's been awhile since I've blogged anything, so I thought a nice way to get back in the game was to post a BeginAnimation method.  Sometimes you just want to start a simple animation in code, and the Silverlight way of doing it through storyboards just seems like more code that you feel like typing, which is where the WPF-inspired BeginAnimation comes in.  Turns out you can do a pretty good BeginAnimation API yourself, using C# extension methods -- just drop the following code into your project:

    public static class Helper
    {
        public static void BeginAnimation(this FrameworkElement e, string prop, Timeline t)
        {
            var sb = new Storyboard();
            e.Resources.Add(sb);
            sb.Children.Add(t);
            Storyboard.SetTarget(sb, e);
            Storyboard.SetTargetProperty(sb, prop);
            sb.Begin();
        }
    }

Which can be used:
                var tb = (TextBox)sender;
                var an = new DoubleAnimation();
                an.From = 100;
                an.To = 200;
                tb.BeginAnimation("Height", an);
 
(Okay, it doesn't handle all the corner cases quite like WPF, in particular calling BeginAnimation more than once on the timeline doesn't work, and the string parameter will need to change to DependencyProperty in SL beta 2 once we fix the Silverlight Storyboard.TargetProperty signature to match WPF, but the extension method does handle the most common case...)

Posted by nkramer | 2 Comments

Last conference for awhile...

I got a chance to speak about Silverlight at the TechReady conference this week (same talk I gave at MIX), I love to talk about Silverlight and it's always great to talk with people who are in the trenches using or thinking about using your stuff. TechReady is an interesting conference, it's like TechEd except with all-Microsoft attendees and a few talks thrown in on subjects like "Linux compete" and "selling SQL Server". It's mostly intended for "the field" -- i.e., people not in product development, such as support, consulting, evangelism, etc. Fairly impressive scale, too, there were several thousand attendees and we had the entire Seattle Convention Center.

The conference also marked a personal milestone for me, I've now spoken at every major Microsoft conference (or at least, every conference that's major to my line of work) -- PDC, TechEd, MIX, WinHEC, even gave a talk once at Driver DevCon. I'm not a real outgoing person, but I enjoy giving these talks, I like honing the presentation skills and putting together a good presentation forces you to see things from another perspective. I also get a lot more out of conferences when I'm a speaker, since more people come up to me to talk.

As much as I like the conferences, though, I'm glad I don't have any on my calendar for a while. They take a lot of time to prepare, and every now and then I'd like to work on Silverlight as well as talk about it. <g> It's also been pretty busy year for me, between business & pleasure I've been on the road roughly once a month since December. I like to travel but it takes a toll, I'm really looking forward to sleeping in the same bed for the next couple weeks.

Posted by nkramer | 1 Comments

Drag drop with feedback

I put together a little Silverlight app demonstrating how to drag and drop between different areas of your Silverlight app.  (This isn't drag and drop in the OLE sense, for security reasons Silverlight doesn't support dragging between processes)  I actually did two versions, one in JavaScript and one in C# ((Silverlight 1.0 and Silverlight 1.1 alpha, respectively), although the approach is the same for both.

The experience I wanted to build was that some areas of the application used auto layout, so that when you dropped an item in that area, the item would snap into place.  And I wanted some kind of visual feedback to show where the item would be put.  So I created an element which I called the shadow element, which is that feedback -- shadow element goes where the item will when you release the mouse button.  So when the mouse moves during a drag, we update the location of the shadow element:

 function handleMouseMove (sender, args) {
        var item = sender;
        if (isMouseCaptured) {
            ...
            moveShadow(overPanel);
        }
 }

        function moveShadow(overPanel)
        {
            hideShadow();

            if (overPanel != null && !overPanel.Equals(workspace2)) {
                overPanel.Children.Add(shadowElement);
                arrangePanel(overPanel);
            }
        }

I wanted some of the panels to have auto-layout, a stack panel-like effect, so I wrote a method to do that:

        function arrangeInRow(canvas)
        {
            var next = 10;
            var i;
            for (i = 0; i <canvas.Children.count; i ++) {
                var element = canvas.children.GetItem(i);
                if (element.toString() == "Ellipse") {
                    var child = element;
                    child["Canvas.Left"] = next;
                    child["Canvas.Top"] = 10;
                    next += child.Width;
                    next += 10;
                }
            }
        }

(The if statement in there is a bit of a hack, I was lazy when I wrote the xaml and so my container had some elements I wanted to be laid out -- the ellipses -- and some elements I didn't want layout to move...)  Dave Relyea has a series of layout samples on http://blogs.msdn.com/devdave/archive/2007/05/17/silverlight-1-1-alpha-layout-system-and-controls-framework.aspx.

As the mouse moves around, you need to figure out what it's over.  Scott Barnes describes a technique for doing this in http://blogs.msdn.com/msmossyblog/archive/2007/06/16/performing-a-hittest-with-silverlight.aspx, my code is slightly different since I wrote the first iteration of couple months ago, but it's the same technique.

As you drag an item between containers, you want the item to be on top of all the other containers.  If you don't do anything special, the item you're moving around will remain part of its original container, and the item will be part of its container's z-order -- so your item will appear on top of some containers but underneath others.  Simplest solution is to change the item's parent, remove it from its container and add it to the root element.  When you do that, you'll need to adjust the Canvas.Left and Canvas.Top properties, which is what this function does:

        function changeParentKeepPosition(item, newParent, e)
        {
            var parent = item.getParent();
            item["Canvas.Top"] = translateY(item["Canvas.Top"], parent, newParent, e);
            item["Canvas.Left"] = translateX(item["Canvas.Left"], parent, newParent, e);
            item.getParent().Children.Remove(item);
            newParent.Children.Add(item);
        }

I wrote the C# version of first, in that version there's a single Translate method that takes a Point.  Unfortunately, creating points in JavaScript is fairly awkward (and createFromXaml doesn't support <Point>), so easiest solution for me was to write separate translateX and translateY functions.  This ended up being the single biggest difference between the JavaScript and C# versions...

        function translateX(point, from, to, e)
        {
            var fromPoint = e.GetPosition(from);
            var toPoint = e.GetPosition(to);
            var delta = fromPoint.X - toPoint.X;
            var result = point - delta;
            return result;
        }

        function translateY(point, from, to, e)
        {
            var toPoint = e.GetPosition(to);
            var fromPoint = e.GetPosition(from);
            var delta = fromPoint.Y - toPoint.Y;
            var result = point - delta;
            return result;
        }

where e is any mouse eventargs...  It's an interesting little trick, although GetPosition was designed for getting the mouse position, with a couple extra lines you can turn it into a general-purpose coordinate transformation routine like the above.

Source code for my TechEd RIA talk

The latest iteration of the FantasyBaseball demo... the end result looks virtually identical, but the underlying code has been cleaned up in a couple different ways.  I simplified the solution/project set up significantly, cutting it down from four projects to two.  And I cleaned up the chart control, got it down to under 400 lines and fixed a few bugs in the process.  I could probably trim a few more lines out of the chart, I spent a few extra lines trying hard to reuse existing elements rather than creating new ones, but when all was said and done that "optimization" didn't actually speed anything up...  Just goes to show when you're doing performance work why it's so important to measure the impact of your changes, not everything that sounds fast actually is.

 

Slides & video for my MIX talk

I had several requests for the slides, I've posted them as an attachment.

The video for the talk (I haven't had a chance to watch it myself) is:
  • Building Silverlight Applications using .NET (Part 1)
  • Building Silverlight Applications using .NET (Part 2) 
  • Source code for my MIX demo

    It's been a fun conference, getting to show off all the things we've been working on. And as usual, its also been a lot of work (both by me and the rest of the Silverlight team).  So before I get completely out of conference mode, I wanted to post Jamie + my fantasy baseball demo + chart. As usual, its a demo so we didn't get every detail perfect; something I'd like to clean up is the chart control.  I went back and forth on whether I wanted a general chart engine or one specific to the app, so its not entirely consistent. But still useful...
    Posted by nkramer | 19 Comments

    Attachment(s): FantasyBaseball.zip

    Nick on Silverlight & WPF

    Yup, it's official, WPF/E is now Silverlight. Once upon a time, I used to get annoyed at the marketing department for these changes. "Why can't these guys just pick a name and stick with it? And why is it such a big secret until we officially release it? It's just a name, how important could it be?" But, over time, I've learned to appreciate why marketing does this (even if it does cause me occasional grief).

    A lot of marketing is about finding a way to explain the essence of the product to people really, really fast. There's a lot of products in the world, far too many for any one person to pay attention to all of them. You don't even have enough time to spend 30 seconds on each of them -- you want to know whether that product is relevant, in three words or less. And that's where the marketing department shines, finding names where your gut reaction is the correct one. Quick, what operating system does Windows Presentation Foundation run on?

    Another thing I've come to accept is that like anything else, it takes time and multiple iterations to get the marketing right. What we now call .Net 3.0 was initially WinFX, until we learned how much it was confusing people who were using .Net.  And while you can do a lot of planning up front, there's no substitution for testing a name in the real world.

    The final mystery to me was why the secrecy.  Are we really worried about someone taking the name Windows Presentation Foundation before we get it?  Sure, we want to get the domain name & trademark before someone else squats on them. But mostly it's about creating the most "buzz", by which I mean free advertising. All that news coverage of a product announcement, that's worth millions of dollars of advertising in the amount of customer awareness it generates.

    So while I'm not thrilled about updating the name of my blog yet again, I sleep better knowing that we have smart people making sure we get the most out of our product names.

    Posted by nkramer | 1 Comments

    Getting mouse positions

    John points out that WPF/E's mouse event.X/Y are in the coordinate space of the browser plug-in, which can be pretty awkward to work with if your element is rotated, scaled, or otherwise transformed.  In February CTP, unfortunately there's no way of transforming the coordinates into the space of your choice, short of writing the matrix multiplication math yourself.  I ran into this as well while writing the slider control, it's definitely ugly, we plan to provide an API for this in a future CTP.

    Posted by nkramer | 1 Comments

    Zip file for the mini-framework

    I zipped up all the different files I use for my WPF/E mini-framework.  I also threw in a couple other controls I've been working on -- slider, hyperlinks, and "animated button" (really just a test case to prove that the framework handles storyboards inside control definitions).  Keep in mind you need to run this over http, not over a file: URL...

    I should also confess before someone calls me on it that not everything I did here is best practice:

    • I should have used a TextBlock instead of Glyphs, that way I wouldn't have had to include various font files in the zip
    • I should have referred to the official aghiost.js instead of having my own copy

       Stay tuned for the service pack <g>

      Posted by nkramer | 0 Comments

      Attachment(s): commoncontrols.zip

      Writing controls in WPF/E

      A while back, I promised to share some tips and tricks about writing controls in the WPF/E CTP (yeah, I apologize, it's been a busy year).  A lot of the SDK samples make use of a little framework that Peter Blois wrote which uses JavaScript's eval to create a system for hooking up delegates/event callbacks in code.  That's a perfectly good way of doing things, but I prefer a little more of a xaml-oriented approach, so I wrote my own little framework to allow controls to be defined in xaml.  Let's walk through an example of a simple button control.  The xaml for the button looks like:

      <!-- Button.xaml -->
      <Canvas
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
          x:Name="button"
          MouseLeftButtonDown="javascript:button_MouseLeftButtonDown"
          MouseLeftButtonUp="javascript:button_MouseLeftButtonUp"
          MouseEnter="javascript:button_MouseEnter"
          MouseLeave="javascript:button_MouseLeave"
          >
        <Canvas.RenderTransform>
          <TransformGroup>
            <TranslateTransform X="0" Y="0" x:Name="transform"/>
          </TransformGroup>
        </Canvas.RenderTransform>
        <Rectangle Stroke="#FF000000" Fill="sc#1, 0.8123474, 0.8123474, 0.8123474"
            Width="128.8" Height="56" x:Name="rectangle"/>
        <Rectangle Stroke="sc#1, 0.912730157, 0.37122494, 0.17111966" StrokeThickness="5"
            Width="126.8" Height="54" Canvas.Left="1" Canvas.Top="1"
            Opacity="0"
            x:Name="highlight"/>

        <Glyphs Fill="black" FontRenderingEmSize="20" FontUri="segmcsb.ttf"
            OriginY="35" OriginX="25" UnicodeString="Button!" >
        </Glyphs>
      </Canvas>

      Which we could then use in our larger application xaml like this:

      <Canvas ...>
        <Canvas Loaded="javascript:MakeButton" Canvas.Top="100" Canvas.Left="20"/>
       
        <Canvas Loaded="javascript:MakeButton" x:Name="button2"
            Canvas.Top="100" Canvas.Left="320"
        />
      </Canvas>

      So the button is really a canvas element, which by calling the framework function MakeButton() takes on the look and behavior of a button control.  MakeButton() is defined as:

      function MakeButton(canvas) {
          var buttonState = MakeControl(canvas, "Button.xaml");
          buttonState.mouseOver = false;
          buttonState.pressed = false;
          buttonState.click = null;
      }

      (The "canvas" parameter is the Loaded event's sender parameter)  The heavy lifting is in the MakeControl call, which loads Button.xaml.  Note how in button.xaml, we specified some event handlers,
      <!-- Button.xaml -->
      <Canvas
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
          x:Name="button"
          MouseLeftButtonDown="javascript:button_MouseLeftButtonDown"
          MouseLeftButtonUp="javascript:button_MouseLeftButtonUp"
          MouseEnter="javascript:button_MouseEnter"
          MouseLeave="javascript:button_MouseLeave"
          >

      The corresponding JavaScript (we'll just do one of those methods) is:
      function button_MouseEnter(sender,args)
      {
          GetState(sender).mouseOver = true;
          ... update element tree to look different ...
      }

      Every button needs a little bit of data about whether it's currently being pressed, whether the mouse is over, and what methods you call when it gets clicked.  This is per-button data, we can store this and globals.  Now WPF/E doesn't support expandos, so we can't just write sender.mouseOver = true (although that's certainly on my wish list of features to add).  So we do the next best thing and associate the sender element (the canvas) with an object we can set properties on -- and that's what the framework call GetState(sender) does.

      GetState() is just one of the things that MakeControl sets up for us.  MakeControl also downloads Button.xaml for us (using the standard JavaScript XmlHttpRequest -- at the time I wrote it, the WPF/E "downloader" API didn't exist).  And it gives us named properties for any of the Name'd elements in button.xaml.  E.g.:

      <!-- Button.xaml -->
      <Canvas ...>
        <Rectangle ... x:Name="rectangle"/>

      We can access this by writing GetState(sender).rectangle.

      But wait -- you can have as many buttons in your application as you want, and each one of those buttons has a rectangle with Name="rectangle"?  Do you get name conflicts?  That's the last major duty of MakeControl -- it actually changes your xaml each time it loads a control, to add a unique suffix to each Name'd element -- e.g., rectangle_43.  Which makes GetState(sender).rectangle all the more important, since you don't really want to keep track of whether it's button #43 or button #71.

      So it's a nifty little system.  Of course, it does have some limitations.  The biggest is that it's really hard to do control composition -- i.e., your listbox control contains a scrollbar control contains a button control.  All the work happens inside the Loaded event, and the problem is that WPF/E waits for one Loaded handler to completely finish executing before it fires the next Loaded event.  So MakeListBox will finish before MakeScrollBar is even called, which makes it very difficult to initialize any of the control-specific APIs like button's click event.  (In this respect, Peter's framework is better)

      The other big limitation is that button.xaml has to be served over http, and not over a \\unc path.  After all, it's XmlHttpRequest, not XmlUncRequest.  I haven't found that to be much of a problem with production code, but it always annoys me during development.

      And finally, for your viewing pleasure, here's the complete source for the framework along with a sample button control:

      ///////////////////////////////////////////////////////
      //           Button
      ///////////////////////////////////////////////////////
      /*
      Button APIs:
      state.pressed -- bool
      state.mouseOver -- bool
      state.click -- event handler
      */


      // turn a Canvas into a button (by inserting Button.xaml inside the canvas)
      function MakeButton(canvas) {
          var buttonState = MakeControl(canvas, "Button.xaml");
          buttonState.mouseOver = false;
          buttonState.pressed = false;
          buttonState.click = null;
      }
         
      function button_MouseLeftButtonDown(sender,args)
      {
          sender.CaptureMouse();
          GetState(sender).mouseOver = true;
          GetState(sender).pressed = true;
          UpdateVisuals(sender);
      }

      function button_MouseLeftButtonUp(sender,args)
      {
          sender.ReleaseMouseCapture();
          GetState(sender).pressed = false;
         
          UpdateVisuals(sender);
         
          if (GetState(sender).mouseOver && GetState(sender).click) {
              GetState(sender).click(sender,args);
          }
      }

      function button_MouseEnter(sender,args)
      {
          GetState(sender).mouseOver = true;
          UpdateVisuals(sender);
      }

      function button_MouseLeave(sender,args)
      {
          GetState(sender).mouseOver = false;
          UpdateVisuals(sender);
      }

      function UpdateVisuals(button) {
          //background
          var state = GetState(button);
         
          if (state.pressed && state.mouseOver) {
              state.rectangle.Fill = "sc#1, 0.548430264, 0.5354195, 0.5354195";
              state.transform.X = 2;
              state.transform.Y = 2;
          } else {
              state.rectangle.Fill = "sc#1, 0.8123474, 0.8123474, 0.8123474";
              state.transform.X = 0;
              state.transform.Y = 0;
          }
         
          // highlight
          if (state.mouseOver || state.pressed) {
              state.highlight.Opacity = 1;
          } else {
              state.highlight.Opacity = 0;
          }
      }

      <!-- Button.xaml -->
      <Canvas
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
          x:Name="button"
          MouseLeftButtonDown="javascript:button_MouseLeftButtonDown"
          MouseLeftButtonUp="javascript:button_MouseLeftButtonUp"
          MouseEnter="javascript:button_MouseEnter"
          MouseLeave="javascript:button_MouseLeave"
          >
        <Canvas.RenderTransform>
          <TransformGroup>
            <TranslateTransform X="0" Y="0" x:Name="transform"/>
          </TransformGroup>
        </Canvas.RenderTransform>
        <Rectangle Stroke="#FF000000" Fill="sc#1, 0.8123474, 0.8123474, 0.8123474"
            Width="128.8" Height="56" x:Name="rectangle"/>
        <Rectangle Stroke="sc#1, 0.912730157, 0.37122494, 0.17111966" StrokeThickness="5"
            Width="126.8" Height="54" Canvas.Left="1" Canvas.Top="1"
            Opacity="0"
            x:Name="highlight"/>

        <Glyphs Fill="black" FontRenderingEmSize="20" FontUri="segmcsb.ttf"
            OriginY="35" OriginX="25" UnicodeString="Button!" >
        </Glyphs>
      </Canvas>


      // common infrastructure for controls

      ///////////////////////////////////////////////////////////////
      // xaml downloading logic

      // smooth over various browser inconsistencies
      function createXMLHttpRequest(){
          var httprequest = false;
          if (window.XMLHttpRequest) { // if Mozilla, Safari etc
              httprequest = new XMLHttpRequest();
              if (httprequest.overrideMimeType)
                  httprequest.overrideMimeType("text/xml");
          }
          else if (window.ActiveXObject){ // if IE
              try {
                  httprequest=new ActiveXObject("Msxml2.XMLHTTP");
              }
              catch (e){
                  try{
                      httprequest=new ActiveXObject("Microsoft.XMLHTTP");
                  }
                  catch (e){}
              }
          }
          return httprequest;
      }

      function downloadText(url) {
          var request = createXMLHttpRequest();
          request.open("GET", url, false);
          request.send(null);
          if (request.status == 200) {
              var text = request.responseText;
              return text;
          }
          else {
            alert("Error loading "+url);
          }
      }

      // synchronously loads the xaml and returns the root DependencyObject
      function downloadXaml(url) {
          var text = downloadText(url);
          var element = wpfeControl.createFromXaml(text);
          return element;
      }

      // the callback is called if/when the download is completed successfully,
      // and receives the parsed DependencyObject as its parameter
      function downloadXamlAsync(url, callback) {
          var request = createXMLHttpRequest();

          request.onreadystatechange = function (evt) {
              if (request.readyState == 4) {
                  if (request.status == 200) {
                      var text = request.responseText;
                      var element = wpfeControl.createFromXaml(text);
                      callback(element);
                  }
                  else {
                    alert("Error loading "+url);
                  }
              }
          };
             
          request.open("GET", url);
          request.send();
      }


      ///////////////////////////////////////////////////////////////
      // control initialization logic

      // usage: partsToStates[interestingobject.name] = new Object();
      // then partsToStates[interestingobject.name].property = foo;
      var partsToStates = new Object();

      // takes an element inside a template, and returns the
      // corresponding control's state object.
      function GetState(element) {
          var name = element.Name;
          if (name == "") {
              throw "element must be named to have a state object";
          }
          return partsToStates[name];
      }

      var containersToStates = new Object();

      // Given a container (canvas), gets the state object for the
      // control this container contains
      function GetControlForContainer(canvas) {
          var name = canvas.Name;
          if (name == "") {
              throw "element must be named to have a state object";
          }
          return containersToStates[name];
      }

      // used to generate unique IDs
      var controlCounter = 0;

      // Return value is xaml with names changed.
      // Second parameter is an output parameter, list of names found
      function RenameElements(xaml, namesFound) {
          var result = "";
          while (true) {
              var regexp = /((\s|x:)((Storyboard.)?Target)?Name=('|"))(\w+)('|")/;
              var matches = regexp.exec(xaml);
              if (matches == null) {
                  result += xaml;
                  break;
              }

              var name = matches[6];
              var newname = name + "_" + controlCounter.toString();

              var isTargetName = (matches[4] != "");
              if (!isTargetName) {
                  namesFound[name] = newname;
              }

              result += xaml.substring(0, matches.index)
                        + matches[1] + newname + matches[7];
                       
              // matches.lastIndex doesn't exist on Firefox
              xaml = xaml.substring(matches.index + matches[0].length);
          }

          return result;
      }

      // to do:
      // parameters in markup
      // check that nested controls works
      // animations inside controls
      // more robust parser for the xaml renaming

      // returns the control's state object
      function MakeControl(canvas, xamlurl) {
          var xaml = downloadText(xamlurl);
          var names = new Object();
          xaml = RenameElements(xaml, names);
          var controlID = controlCounter;
          controlCounter++;
         
          var state = new Object();
          state.createFromXaml = function(text) {
              return wpfeControl.createFromXaml(text);
          }
          containersToStates[canvas.Name] = state;
          state.container = canvas;
          state.templateRoot = wpfeControl.createFromXaml(xaml);
          canvas.Children.Add(state.templateRoot);

          for (var name in names) {
              var fullname = names [name];
              var element = canvas.FindName(fullname);
              var sanityCheck = element.Name;
              if (fullname != sanityCheck) {
                  throw "for some reason, the names don't match";
              }
              state[name] = element;
              partsToStates[fullname] = state;
          }
         
          return state;
      }


      /*
      CommonControls.js
      This file demonstrates the use of some common controls
      */

      function root_Loaded(sender) {
          // root_Loaded will run before its children's Loaded events.
          // But the initialization we want to do requires those children's
          // Loaded events to run first.  Therefore, we put a dummy element
          // at the end of the tree, so we can get a Loaded event after
          // everything else has been loaded.
      }

      function delayedInitialization(sender) {
          // Intentionally hook up only the left button
          var button2 = sender.FindName("button2");
          GetControlForContainer(button2).click = function(sender) { alert("hello world"); };
      }

      function ErrorHandler(line, col, hr, string)
      {
          var str = "("+line+","+col+"): "+string+"\n";
          str += "HRESULT: "+hr;
          alert(str);
      }

      <!-- CommonControls.xaml -->
      <Canvas Width="700" Height="700"
          xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
          xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
          x:Name="root" Loaded="javascript:root_Loaded"
        >

        <Canvas Loaded="javascript:MakeButton" x:Name="b1" Canvas.Top="100" Canvas.Left="20"/>
        <Canvas Loaded="javascript:MakeButton" x:Name="button2"
            Canvas.Top="100" Canvas.Left="320"
        />
       
        <!-- hack to get a Loaded event after all the controls have been initialized -->
        <Canvas Loaded="javascript:delayedInitialization" Opacity="0"/>
      </Canvas>

      Posted by nkramer | 5 Comments

      The scrum methodology

      I hear a lot about scrum these days, not the least of which because WPF/E uses scrum. Personally, I dislike any capital M Methodology, and there's something about the scrum vocabulary that really irks me. But I'll grudgingly admit, it works. There's several useful ideas in scrum and agile methodologies, but I think by far the most important is this: build the simplest thing that might possibly work -- then give it to the customer and iterate. If it's too simple, you can always make it more complicated later. It's rarely as hard to add functionality later as we think it is -- nor do we ever understand the customer's needs as well as we think we do.

      One of the scrum criticisms I've heard is that it can't work for building platforms. That agility means breaking changes, and breaking changes are catastrophic to your customers. That's a legitimate concern, breaking changes really are costly, but I think it's manageable provided you aren't too religious about scrum. I think when building a widely used API, you need to be very cautious about exposing more surface area than you are prepared to support. (For example, not making methods virtual unless you really mean it) And of course, build the simplest thing that might possibly work. If the simplest thing doesn't fit in a milestone ("sprint"), build it anyway -- there's no point in building something simpler if you know it won't work. Nor should you let scrum be an excuse not to do requirements homework -- the better you understand what the customer needs, the faster you can build it. Just be very humble about how well you can predict what will be needed, and when in doubt, start simple and iterate.

      Posted by nkramer | 1 Comments

      Nick on WPF/E

      Now that we've shipped our first CTP, I can tell you what I've been working on for the last six months. (No points for guessing!) It's been a lot of work but I'm pretty happy with the ctp. It's definitely not perfect, and there's a some important scenarios we don't solve yet -- while it might be technically possible to write a large data entry forms application on WPF/E, I wouldn't want to be the one to do it, not until we have more controls. But it is a snappy little platform, and I'm constantly amazed how far I can get using just WPF/E and a little JavaScript.

      As always, I'd love your feedback. I'm especially interested in feedback on controls and other UI framework functionality -- I'd love to know what kind of apps you'd like to build, what technologies you want to build them with (which controls, what programming languages, what server infrastructure, ...), and what browsers/os'es you'd like to run them on. I'd also love feedback on the input/mouse/keyboard APIs (and yes, we don't have keyboard events yet, we're working on it) -- I've worked on input long enough to have a pretty good idea of what people need, but no matter how much you know there's always a customer requirement you haven't heard yet. So don't hesitate to post a comment or send mail (nkramer at Microsoft), I look forward to hearing from you!

      Posted by nkramer | 12 Comments

      JavaScript, the underappreciated language

      I've spent a good part of my career writing compilers, editors, and runtime libraries, so I've formed some fairly strong opinions about what makes a good programming language.  Which is why if you'd told me six months ago that I would be programming inJavaScript -- and liking it -- I would have thought you were crazy.  But I've been doing a lot of coding in JavaScript lately, and I have to admit, it's actually a pretty well-designed language and pleasant to use.

      To me, what makes JavaScript not just another poorly designed scripting language is lexical scoping, closures, and functions as first-class objects.  You can assign a function to a variable:

      var foo = function() {...};

      And you can pass that function around like any other object.  You can define functions within functions, and when you do that you can create closures that use the local variables of the outer function:

      function outer() {
          var foo = 5;
          return function() {return foo;};
      }

      And if you evaluate that function, it will find the right foo, even if you call this function from a place where foo has a different definition -- it's lexically scoped!

      function differentFoo() {
          var foo = 7; //not the same foo!
          var fn = outer();
          fn();
      }

      (Okay, for the purists out there, there are some constructs that aren't lexical -- such as with statements -- but for the most part JavaScript code behaves as if it's lexical)

      I also enjoy the traditional benefits of scripting -- no compiler, no type signatures (especially, remembering which subclass of EventArgs a particular event requires), optional parameters, etc.

      It's not all roses, of course.  You sometimes need to resort to eval for relatively simple things (like #include), which as a programming tools guy makes me cringe -- eval is Turing complete and blocks tools from understanding the code.  And I've been bitten occasionally by setting foo.Property instead of foo.property, and accidentally creating a totally new property.  (Although it happens much less often than I thought it would -- unlike many scripting languages, JavaScript detects undefined variables, just not undefined properties)  And while in many ways I like the idea of prototype-based inheritance, it doesn't always work -- sometimes, you really need a base class constructor, and at that point you're inventing your own language mechanisms.

      But, all things considered, I'm actually pretty impressed with the design, and I'm looking forward to seeing what the next version of EcmaScript brings.

      Posted by nkramer | 5 Comments

      XML heresies

      Right before I went on vacation, I wrote a little code to do syntax coloring for XML, which reminded me of my mixed feelings about XML.  Don't get me wrong, there's enough momentum behind it that these days it's the right answer for almost any text format.  But at the same time, it has some significant design flaws that drive me nuts.  So at risk of getting flamed back to the Stone Age by the XML experts, here goes.

      My biggest gripe is that its data model just doesn't map very well to programming languages.  The biggest issue by far is the attribute/element distinction.  If I have a class with a "foo" property, in XML should I make foo into an attribute, or a element?  (Solving this dilemma is probably the single biggest reason for XAML's existence)  One of the consequences of this is that a lot of information ends up getting crammed into attribute values, and those attribute values have a lot of structure to them -- structure that's not captured in XML.  To compound matters, even simple data types like floating-point numbers are left out of the XML spec, so every application is left to its own devices to parse these.  (Quick, what's the grammar for a floating-point number?  Did you remember exponents and signs?)

      But there are other representational difficulties as well -- what if your data structure is not a pure tree, but rather has sharing or cycles?  Sure, you can give the shared element an ID and refer to it by name, but now you're inventing more semantics above and beyond XML.  Arguably even XPointer doesn't completely solve the problem, since it supplies only named references, rather than anonymous "everyone's reference is created equal and no reference owns the object more than anyone else"-style references which programming languages use.

      One of the other odd things is that there's very little you can do with a generic piece of XML.  Suppose you have a file that you know conforms to XML, but not necessarily any of the other standards (namespaces, schemas, etc.) -- what can you do with that?  Remarkably little.  You can't understand the true data structure of the document (see above).  You don't know when two element names refer to the same element because you don't understand namespaces.  You can't provide a structure editor (think IntelliSense) because you don't know what's valid and what's not (unless the file specifies a DTD rather than XSD or something else, but then DTD's bring their own problems).  You can't move elements around, or do a consistent rename of namespaces.  You really shouldn't even insert constructs the XML specification says are safe, such as comments and processing instructions, because the XML DOM makes it so easy for programs to accidentally care about some syntactic detail they shouldn't.  Nor can you write a tool that adds indentation to the XML, because whitespace may or may not be significant (and setting xml:space="default" doesn't help -- XML 1.0 doesn't specify what this actually means).  About the only thing I can think of you could do with generic XML is syntax highlighting, and a viewer that lets you expand and collapse elements -- not exactly earth shattering.

      Speaking of syntax highlighting, the XML lexical grammar (as opposed to a higher level, syntactic grammar) is a bit of an oddball.  Okay, XML is defined using only a single level (context-free) grammar rather than separate lexical and syntactic specifications, but you can't use that for syntax highlighting...  If you want a lexical grammar that is useful for syntax highlighting, what you find is that lexing is context-sensitive, because foo="bar" will be colored differently if it's part of an element's attribute list or if it appears as text content.

      XML modularization is also a bit funny.  On the one hand, XML puts core functionality such as namespaces and schemas into a separate specification.  But XML also has some functionality that rarely gets used today.  DTD's aren't used because they aren't very expressive, don't work with namespaces, and can infinitely recurse creating a potential denial of service attack.  Processing instructions (e.g., <? SomePI ?>) have no well-defined meaning, and quite a few XML processors out there choke on them.  Custom entities (beyond the standard &lt; &gt; &amp; &apos; and &quot;) solve who knows what problem.  And XML supports a wide variety of encodings that I've never heard of anyone using. 

      And while I love using other people's XML parsers so I don't need to write my own, I've always wondered who the DOM's designers were targeting.  For most people, the DOM gives out way too much information -- I don't want to know about comments and processing instructions, can't you strip that information out so my program doesn't accidentally trip on them?  I'd much rather have something like the Infoset (although even that doesn't abstract away quite as much as I might like).

      So what do I like about XML?  Well, I like that it's a standardized and widely implemented format, with parsing libraries readily available.  And despite a few quirks, XML namespaces are fairly well done, and provide the foundation for versioning.  And XSD may not be perfect, but it's good enough to represent most of the information you want in IntelliSense most of the time.  Still, I can't help but wonder, if someone turned in the XML design as their college homework, what grade would they get?

      Posted by nkramer | 5 Comments

      Syntax coloring for xaml

      I wrote some code that would generate syntax coloring for xaml (and for that matter, most other XML), figured I would share it out.  Some assembly required:

      using System;
      using System.Collections.Generic;
      using System.Text;
      using System.Diagnostics;

      namespace BuildQuickStart
      {
          /*
           * this file implements a mostly correct XML tokenizer.  The token boundaries
           * have been chosen to match Visual Studio syntax highlighting, so a few of
           * the boundaries are little weird.  (Especially comments) known issues:
           *
           * Doesn't handle DTD's
           * mediocre handling of processing instructions <? ?> -- it won't crash,
           *      but the token boundaries are wrong
           * Doesn't enforce correct XML
           * there's probably a few cases where it will die if given in valid XML
           *
           *
           * This tokenizer has been designed to be restartable, so you can tokenize
           * one line of XML at a time.
           */
         
          //enum TokenColors
          //{
          //    Punctuation, StringLiteral, ElementName, AttributeName, Comment, Normal
          //}

          enum XmlTokenKind : short
          {
              Open, // <
              Close,//>
              SelfClose,// />
              OpenClose,// </
              ElementName,
              ElementWhitespace,//whitespace between attributes
              AttributeName,
              Equals, // inside attribute
              AttributeValue, // attribute value
              CommentBegin, // <!--
              CommentText,
              CommentEnd, // -->
              Entity, // &gt;
              OpenProcessingInstruction, // <?
              CloseProcessingInstruction, // ?>
              CDataBegin, // <![CDATA[
              CDataEnd,// ]]>
              TextContent,
              //WhitespaceContent, // text content that's whitespace.  Space is embedded inside
              EOF, // end of file
          }

          // Used so you can restart the tokenizer for the next line of XML
          enum XmlTokenizerMode
          {
              InsideComment,
              InsideProcessingInstruction,
              AfterOpen,
              AfterAttributeName,
              AfterAttributeEquals,
              InsideElement, // after element name, before attribute or />
              OutsideElement,
              InsideCData,
          }

          struct XmlToken
          {
              public XmlTokenKind Kind;
              public short Length;
              public XmlToken(XmlTokenKind kind, int length)
              {
                  Kind = kind;
                  Length = (short)length;
              }
          }

          // XML tokenizer, tokens are designed to match Visual Studio syntax highlighting
          class XmlTokenizer
          {
              string input;
              int position = 0;
              XmlTokenizerMode mode = XmlTokenizerMode.OutsideElement;

              public static List<XmlToken> Tokenize(string input)
              {
                  XmlTokenizerMode mode = XmlTokenizerMode.OutsideElement;
                  XmlTokenizer tokenizer = new XmlTokenizer();
                  return tokenizer.Tokenize(input, ref mode);
              }

              public List<XmlToken> Tokenize(string input, ref XmlTokenizerMode _mode)
              {
                  this.input = input;
                  this.mode = _mode;
                  this.position = 0;
                  List<XmlToken> result = Tokenize();
                  _mode = this.mode;
                  return result;
              }

              private List<XmlToken> Tokenize()
              {
                  List<XmlToken> list = new List<XmlToken>();
                  XmlToken token;
                  do {
                      int previousPosition = position;
                      token = NextToken();
                      string tokenText = input.Substring(previousPosition, token.Length);
                      list.Add(token);
                  } while (token.Kind != XmlTokenKind.EOF);

                  List<string> strings = TokensToStrings(list, input);

                  return list;
              }

              private List<string> TokensToStrings(List<XmlToken> list, string input)
              {
                  List<string> output = new List<string>();
                  int position = 0;
                  foreach (XmlToken token in list) {
                      output.Add(input.Substring(position, token.Length));
                      position += token.Length;
                  }
                  return output;
              }

              // debugging function
              public string RemainingText
              {
                  get { return input.Substring(position); }
              }

              private XmlToken NextToken()
              {
                  if (position >= input.Length)
                      return new XmlToken(XmlTokenKind.EOF, 0);

                  XmlToken token;
                  switch (mode) {
                      case XmlTokenizerMode.AfterAttributeEquals:
                          token = TokenizeAttributeValue();
                          break;
                      case XmlTokenizerMode.AfterAttributeName:
                          token = TokenizeSimple("=", XmlTokenKind.Equals, XmlTokenizerMode.AfterAttributeEquals);
                          break;
                      case XmlTokenizerMode.AfterOpen:
                          token = TokenizeName(XmlTokenKind.ElementName, XmlTokenizerMode.InsideElement);
                          break;
                      case XmlTokenizerMode.InsideCData:
                          token = TokenizeInsideCData();
                          break;
                      case XmlTokenizerMode.InsideComment:
                          token = TokenizeInsideComment();
                          break;
                      case XmlTokenizerMode.InsideElement:
                          token = TokenizeInsideElement();
                          break;
                      case XmlTokenizerMode.InsideProcessingInstruction:
                          token = TokenizeInsideProcessingInstruction();
                          break;
                      case XmlTokenizerMode.OutsideElement:
                          token = TokenizeOutsideElement();
                          break;
                      default:
                          token = new XmlToken(XmlTokenKind.EOF, 0);
                          Debug.Fail("missing case");
                          break;
                  }
                  return token;
              }

              private bool IsNameCharacter(char character)
              {
                  // XML rule: Letter | Digit | '.' | '-' | '_' | ':' | CombiningChar | Extender
                  bool result = char.IsLetterOrDigit(character)
                  || character == '.' | character == '-' | character == '_' | character == ':';
                  return result;
              }

              private XmlToken TokenizeAttributeValue()
              {
                  Debug.Assert(mode == XmlTokenizerMode.AfterAttributeEquals);
                  int closePosition = input.IndexOf(input[position], position + 1);
                  XmlToken token = new XmlToken(XmlTokenKind.AttributeValue, closePosition + 1 - position);
                  position = closePosition + 1;
                  mode = XmlTokenizerMode.InsideElement;
                  return token;
              }

              private XmlToken TokenizeName(XmlTokenKind kind, XmlTokenizerMode nextMode)
              {
                  Debug.Assert(mode == XmlTokenizerMode.AfterOpen || mode == XmlTokenizerMode.InsideElement);
                  int i;
                  for (i = position; i < input.Length; i++) {
                      if (!IsNameCharacter(input[i])) {
                          break;
                      }
                  }
                  XmlToken token = new XmlToken(kind, i - position);
                  mode = nextMode;
                  position = i;
                  return token;
              }

              private XmlToken TokenizeElementWhitespace()
              {
                  int i;
                  for (i = position; i < input.Length; i++) {
                      if (!char.IsWhiteSpace(input[i])) {
                          break;
                      }
                  }
                  XmlToken token = new XmlToken(XmlTokenKind.ElementWhitespace, i - position);
                  position = i;
                  return token;
              }

              private bool StartsWith(string text)
              {
                  if (position + text.Length > input.Length)
                      return false;
                  else
                      return input.Substring(position, text.Length) == text;
              }

              private XmlToken TokenizeInsideElement()
              {
                  if (char.IsWhiteSpace(input[position]))
                      return TokenizeElementWhitespace();
                  else if (StartsWith("/>"))
                      return TokenizeSimple("/>", XmlTokenKind.SelfClose, XmlTokenizerMode.OutsideElement);
                  else if (StartsWith(">"))
                      return TokenizeSimple(">", XmlTokenKind.Close, XmlTokenizerMode.OutsideElement);
                  else {
                      return TokenizeName(XmlTokenKind.AttributeName, XmlTokenizerMode.AfterAttributeName);
                  }
              }


              //// We break on newlines because that makes it easier for us
              //// to ignore the space after comments
              //private Token TokenizeWhitespaceContent()
              //{
              //    Debug.Assert(char.IsWhiteSpace(input[position]));
              //    bool sawNewline = false;
              //    int i;
              //    for (i = position; i < input.Length; i++) {
              //        if (!char.IsWhiteSpace(input[i])) {
              //            break;
              //        } else if (input[i] == '\n' || input[i] == '\r') {
              //            sawNewline = true;
              //        } else if (sawNewline) {
              //            break;
              //        }
              //    }
              //    Token token = new Token(TokenKind.WhitespaceContent, i - position);
              //    position = i;
              //    return token;
              //}


              private XmlToken TokenizeText()
              {
                  Debug.Assert(input[position] != '<');
                  Debug.Assert(input[position] != '&');
                  Debug.Assert(mode == XmlTokenizerMode.OutsideElement);
                  int i;
                  for (i = position; i < input.Length; i++) {
                      if (input[i] == '<' || input[i] == '&') {
                          break;
                      }
                  }
                  XmlToken token = new XmlToken(XmlTokenKind.TextContent, i - position);
                  position = i;
                  return token;
              }

              private XmlToken TokenizeOutsideElement()
              {
                  Debug.Assert(mode == XmlTokenizerMode.OutsideElement);
                  if (position >= input.Length)
                      return new XmlToken(XmlTokenKind.EOF, 0);

                  switch (input[position]) {
                      case '<':
                          return TokenizeOpen();
                      case '&':
                          return TokenizeEntity();
                      default:
                          return TokenizeText();
                  }
              }

              private XmlToken TokenizeSimple(string text, XmlTokenKind kind, XmlTokenizerMode nextMode)
              {
                  XmlToken token = new XmlToken(kind, text.Length);
                  position += text.Length;
                  mode = nextMode;
                  return token;
              }

              private XmlToken TokenizeOpen()
              {
                  Debug.Assert(input[position] == '<');
                  if (StartsWith("<!--")) {
                      return TokenizeSimple("<!--", XmlTokenKind.CommentBegin, XmlTokenizerMode.InsideComment);
                  } else if (StartsWith("<![CDATA[")) {
                      return TokenizeSimple("<![CDATA[", XmlTokenKind.CDataBegin, XmlTokenizerMode.InsideCData);
                  } else if (StartsWith("<?")) {
                      return TokenizeSimple("<?", XmlTokenKind.OpenProcessingInstruction, XmlTokenizerMode.InsideProcessingInstruction);
                  } else if (StartsWith("</")) {
                      return TokenizeSimple("</", XmlTokenKind.OpenClose, XmlTokenizerMode.AfterOpen);
                  } else {
                      return TokenizeSimple("<", XmlTokenKind.Open, XmlTokenizerMode.AfterOpen);
                  }
              }

              private XmlToken TokenizeEntity()
              {
                  Debug.Assert(mode == XmlTokenizerMode.OutsideElement);
                  Debug.Assert(input[position] == '&');
                  XmlToken token = new XmlToken(XmlTokenKind.Entity, input.IndexOf(';', position) - position);
                  position += token.Length;
                  return token;
              }

              private XmlToken TokenizeInsideProcessingInstruction()
              {
                  Debug.Assert(mode == XmlTokenizerMode.InsideProcessingInstruction);
                  int tokenend = input.IndexOf("?>", position);
                  if (position == tokenend) {
                      position += "?>".Length;
                      mode = XmlTokenizerMode.OutsideElement;
                      return new XmlToken(XmlTokenKind.CloseProcessingInstruction, "?>".Length);
                  } else {
                      XmlToken token = new XmlToken(XmlTokenKind.TextContent, tokenend - position);
                      position = tokenend;
                      return token;
                  }
              }

              private XmlToken TokenizeInsideCData()
              {
                  Debug.Assert(mode == XmlTokenizerMode.InsideCData);
                  int tokenend = input.IndexOf("]]>", position);
                  if (position == tokenend) {
                      position += "]]>".Length;
                      mode = XmlTokenizerMode.OutsideElement;
                      return new XmlToken(XmlTokenKind.CDataEnd, "]]>".Length);
                  } else {
                      XmlToken token = new XmlToken(XmlTokenKind.TextContent, tokenend - position);
                      position = tokenend;
                      return token;
                  }
              }


              private XmlToken TokenizeInsideComment()
              {
                  Debug.Assert(mode == XmlTokenizerMode.InsideComment);
                  int tokenend = input.IndexOf("-->", position);
                  if (position == tokenend) {
                      position += "-->".Length;
                      mode = XmlTokenizerMode.OutsideElement;
                      return new XmlToken(XmlTokenKind.CommentEnd, "-->".Length);
                  } else {
                      XmlToken token = new XmlToken(XmlTokenKind.CommentText, tokenend - position);
                      position = tokenend;
                      return token;
                  }
              }
          }
      }


              static string ColorForToken(XmlToken token, string tokenText)
              {
                  string color = "black";
                  switch (token.Kind) {
                      case XmlTokenKind.Open:
                      case XmlTokenKind.OpenClose:
                      case XmlTokenKind.Close:
                      case XmlTokenKind.SelfClose:
                      case XmlTokenKind.CommentBegin:
                      case XmlTokenKind.CommentEnd:
                      case XmlTokenKind.CDataBegin:
                      case XmlTokenKind.CDataEnd:
                      case XmlTokenKind.Equals:
                      case XmlTokenKind.OpenProcessingInstruction:
                      case XmlTokenKind.CloseProcessingInstruction:
                      case XmlTokenKind.AttributeValue:
                          color = "blue";
                          break;
                      case XmlTokenKind.ElementName:
                          color = "brown";
                          break;
                      case XmlTokenKind.TextContent:
                          color = "black";
                          break;
                      case XmlTokenKind.AttributeName:
                      case XmlTokenKind.Entity:
                          color = "red";
                          break;
                      case XmlTokenKind.CommentText:
                          color = "green";
                          break;
                  }
                  if (token.Kind == XmlTokenKind.ElementWhitespace
                      || (token.Kind == XmlTokenKind.TextContent && tokenText.Trim() == "")) {
                      color = null;
                  }
                  return color;
              }

      // Sample usage:
                  XmlTokenizer tokenizer = new XmlTokenizer();
                  XmlTokenizerMode mode = XmlTokenizerMode.OutsideElement;

                          List<XmlToken> tokens = tokenizer.Tokenize(line, ref mode);
                          List<string> tokenTexts = new List<string>(tokens.Count);
                          List<string> colors = new List<string>(tokens.Count);
                          int position = 0;
                          foreach (XmlToken token in tokens) {
                              string tokenText = line.Substring(position, token.Length);
                              tokenTexts.Add(tokenText);
                              string color = ColorForToken(token, tokenText);
                              colors.Add(color);
                              position += token.Length;
                          }

      Posted by nkramer | 0 Comments
      More Posts Next page »
       
      Page view tracker