« March 2007 | Main | May 2007 »

April 22, 2007

Disposing Virtualizing Stack Panel

A VirtualizingStackPanel is very useful when using WPF -- when items scroll out of the viewable area, they may be destroyed to reclaim the valuable UI resources they are consuming. By default, the VirtualizingStackPanel automatically destroys the virtual tree for items that become reclaimed automatically, without the item being aware at all. Often, items are mapped to a DataTemplate which provide the UI.

I was in need of a way to have the virtualized item know when it was being reclaimed -- as the internals of the object held onto resources that could have been considered wasteful if they were to pointlessly be held (in this case, it was a bitmap). Using a simple overridden method of the VirtualizingStackPanel, OnCleanUpVirtualizedItem, it's easy to inform the displayed item when its visuals are being removed.

  public class VirtualizingStackPanelEnh : VirtualizingStackPanel
  {
    protected override void OnCleanUpVirtualizedItem(CleanUpVirtualizedItemEventArgs e)
    {
      IDisposable disp = e.Value as IDisposable;
      base.OnCleanUpVirtualizedItem(e);
      if (disp != null)
      {
        disp.Dispose();
      }
    }
  }

Above is my simple implementation. Items which want to know they no longer are being shown can implement IDisposable. When the Dispose method is called, clean up any internal resources that may be wasteful if the item is not being displayed. Of course, your items don't have to implement IDisposable, it could be anything you want, I have an implementation that just calls a "Cleanup" method on a known object as well. (It could be an event that is raised to the list item as well).

    <Style x:Key="PhotoListBoxStyle" TargetType="{x:Type ListBox}">
      <Setter Property="Template">
        <Setter.Value>
          <ControlTemplate TargetType="{x:Type ListBox}">
            <Border Background="{TemplateBinding Panel.Background}"
                BorderBrush="{TemplateBinding Border.BorderBrush}"
                BorderThickness="{TemplateBinding Border.BorderThickness}">
              <ScrollViewer HorizontalScrollBarVisibility="Auto" Template="{DynamicResource ScrollViewerControlTemplate1}">
                <local:VirtualizingStackPanelEnh IsItemsHost="True" Orientation="Horizontal"/>
              </ScrollViewer>
            </Border>
          </ControlTemplate>
        </Setter.Value>
      </Setter>
    </Style>

To use the new stack panel, I created a new ControlTemplate for a ListBox and embedded the VirtualizingStackPanelEnh directly inside the ScrollViewer.

April 15, 2007

CommandTarget, MenuItem, ContextMenus, and Binding, oh my!

MenuItems can be bound directly to a command. The MenuItem is enabled or disabled based on several factors, including:

  • Does a control have focus?
  • Does the Command have a CommandTarget specified?
  • Does the CanExecute on the CommandBinding return True?

All in all -- not too bad.

That allows me to do something like this:

  <Window.CommandBindings>
    <CommandBinding Command="ApplicationCommands.Close" 
CanExecute="AlwaysCanExecute"
Executed="ApplicationCommandsClose" /> </Window.CommandBindings> <Window.InputBindings> <KeyBinding Key="Escape" Command="ApplicationCommands.Close" /> </Window.InputBindings> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <Menu> <MenuItem Header="_File"> <MenuItem Command="ApplicationCommands.Close" > <MenuItem.Header> Exit </MenuItem.Header> </MenuItem> </MenuItem> </Menu>
    private void AlwaysCanExecute(object sender, CanExecuteRoutedEventArgs e)
    {
      e.CanExecute = true;
    }

private void ApplicationCommandsClose(object sender, ExecutedRoutedEventArgs e)
{
Close();
}


I always want the Exit menu enabled (at least here), so I've created a method called "AlwaysCanExecute" that always returns True.


Trouble happens though if I start using a ContextMenu on a control that does not get focus. In this example an Image:

    <Image Width="128" Height="128" Source="myimage.png" Stretch="Fill">
<Image.ContextMenu>
<ContextMenu>
<MenuItem Command="ApplicationCommands.Close">
<MenuItem.Header>
Exit
</MenuItem.Header>
</MenuItem>
</ContextMenu>
</Image.ContextMenu>
</Image>

Here I've added a context menu to an image. If I run this application, the MenuItem for Exit is always disabled. The Window CommandBindings are still set.


There seem to be two work arounds.


The first thing I tried though was to use a Binding syntax to set the CommandTarget to a particular element.

CommandTarget="{Binding ElementName=MainWindow}"

The error message that happens is this:



System.Windows.Data Error: 4 : Cannot find source for binding with reference 'ElementName=MainWindow'. BindingExpression:(no path); DataItem=null; target element is 'MenuItem' (Name='mnuClose'); target property is 'CommandTarget' (type 'IInputElement')


This really should have work. I think it's a bug (but would like to hear differently).


Oddly, if I, using this:


 

<MenuItem Command="ApplicationCommands.Close" x:Name="mnuClose" 
CommandTarget="{Binding ElementName=MainWindow}" >
<MenuItem.Header>
Exit
</MenuItem.Header>
</MenuItem>

And then in code, set the CommandTarget explicitly:

    public Window1()
{
InitializeComponent();
mnuClose.CommandTarget = this;
}

It works fine. So, the Binding is the problem. That's solution one. If you don't mind adding code and giving the context menuitems a name, you're good to go.


However, I wasn't satisfied.


It just can't seem to find the right scope to locate the elements (hence the error). I tried many Binding combinations and couldn't seem to find one that worked.


So, in the hunt to find an alternative, I discovered the easiest work around that involves zero code is to duplicate the CommandBindings like this:

      <Image.ContextMenu>
<ContextMenu>
<ContextMenu.CommandBindings>
<CommandBinding Command="ApplicationCommands.Close"
CanExecute="AlwaysCanExecute"
Executed="ApplicationCommandsClose" />
</ContextMenu.CommandBindings>
<MenuItem Command="ApplicationCommands.Close" >
<MenuItem.Header>
Exit
</MenuItem.Header>
</MenuItem>
</ContextMenu>
</Image.ContextMenu>

I've added the Commands (by adding a Binding) that I'm using in the ContextMenu directly to the ContextMenu CommandBindings property. I'm calling the same methods as I called for the Window.CommandBindings (as the logic of when and what to do hasn't changed). It's not elegant as I'd like and I suppose I could have written a helper class to guarantee the command bindings have been properly duplicated, but ... that's left as an exercise to the reader!


I like setting the CommandBindings in the application level so this bugs me that I need to duplicate them and keep them up to date. I can't imagine that Microsoft would declare this anything but a bug. If someone knows better leave a comment if they're enabled or send me an e-mail to coder @ this website.


If the control can get focus, this all just works as expected without any trickery. Enjoy.

April 14, 2007

Extracting thumbnails from a JPEG/JPG in .NET 3.0

.NET 3.0 contains a vast array of media related classes. So many that at times it can be overwhelming! Classes that relate to image handling and manipulation number greater than ten!

In this post though, I'll discuss only a few of those that relate to retrieving or creating a thumbnail of a JPG/JPEG. If you need to create only a few thumbnails and want your code to be entirely in XAML, the easiest way to do that is to use the BitmapImage class.

Instead of specifying the Source directly on the Image element, you need to set the Source property to a BitmapSource object. (If you pass a file name or web address directly,  a converter is used to automatically create a BitmapImage for you as a coding-time saving technique.).

 

<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
  xmlns:sys="clr-namespace:System;assembly=mscorlib" 
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" >
  <Canvas>
  <Border Canvas.Left="20" Canvas.Top="20" 
    Background="White" BorderBrush="Silver" BorderThickness="1" Padding="8" Width="Auto" Height="Auto">
  <Border.BitmapEffect>
    <DropShadowBitmapEffect />
  </Border.BitmapEffect>
  <Image Width="200">
    <Image.Source>
      <BitmapImage DecodePixelWidth="200"  
         UriSource="D:\temp\needle.JPG" />
      </Image.Source>
  </Image>
  </Border>
  </Canvas>
</Page>

(If you want to use this sample in XAMLPad, make sure you change the UriSource property to a file on your computer). The result should look something like this:

The BitmapImage class has two helpful properties to reduce the size of the image. But you might be saying to yourself, why can't I just use the Width property on the Image class? Sure, that works. If the image isn't very big, nothing bad will happen. However, if the image is large, you're using memory unnecessarily. The image is loaded and cached in memory, and then transformed to the necessary size. So, if you have an image that is 2000x1500, and you want to show it at 200x150, you're using 10 times the memory you actually need! That hardly seems like a good idea. Not only will it take more valuable memory, it's going to be a bit slower as the memory is copied and the image is shrunk to fit. That's where the two useful properties I hinted at come in, DecodePixelWidth and DecodePixelHeight. You'll only need to use one of them as they proportionally reduce the size of your image when you just set one. If you know that the image is going to be smaller than the original, do your users a favor and use them.

What happens if you set the DecodePixelWidth/Height too low and end up displaying the image at a larger size? Depending on the ratio, the effect can be subtle to dramatic. For a dramatic example, I've used the same code as above, but changed the DecodePixelWidth to 20:

That might be the Space Needle in Seattle or a picture of your skinny uncle. Be sure that you set the DecodePixelWidth/Height to a size that makes sense for your application.

That's one way to create a thumbnail.

For my recent PhotoScroll demo application, I wanted more control -- I wanted a class that could dynamically provide the images and create a thumbnail (and optionally extract it from a JPG if the thumbnail was embedded). That required some tricks.

I won't paste all of the code here, for the complete code, download the sample application available on the other post.

The code uses a couple of simple "oops, we don't need to load anymore" exit points (the variable _loading), so you can ignore those here. After the code, I'll walk through the important parts.

 
protected virtual void DoLoadImage(object o)
{
  // if we don't need to load, drop out ...
  if (!_loading) { return; }

if (System.IO.File.Exists(_filename))
{
MemoryStream mem = null;
try
{
byte[] buffer = File.ReadAllBytes(_filename);
// by reading the data into an in-memory buffer, we prevent the file from being read in the UI thread
// -- which speeds up access dramatically!
mem = new MemoryStream(buffer);

if (!_loading)
{
mem.Dispose();
return;
}

// we need to read the metadata in the UI thread, so we'll open the bitmap and grab it using a
// delegate/invoke. (since the user is waiting for this, there's little benefit to doing this
// as an async invoke).
Application.Current.Dispatcher.Invoke(DispatcherPriority.Normal, (NoArgumentDelegate)delegate()
{
bool foundThumbnail;
BitmapDecoder imgDecoder = BitmapDecoder.Create(mem, BitmapCreateOptions.None, BitmapCacheOption.None);
BitmapFrame frame = imgDecoder.Frames[0];
_imageSource = frame.Thumbnail;
foundThumbnail = _imageSource != null;

if (!foundThumbnail)
{
// we'll make a thumbnail image then ... (too bad as the pre-created one is FAST!)
TransformedBitmap thumbnail = new TransformedBitmap();
thumbnail.BeginInit();
thumbnail.Source = frame as BitmapSource;

// we'll make a reasonable sized thumnbail with a height of 240
int pixelH = frame.PixelHeight;
int pixelW = frame.PixelWidth;
int decodeH = 240;
int decodeW = (frame.PixelWidth * decodeH) / pixelH;
double scaleX = decodeW / (double)pixelW;
double scaleY = decodeH / (double)pixelH;
TransformGroup transformGroup = new TransformGroup();
transformGroup.Children.Add(new ScaleTransform(scaleX, scaleY));
thumbnail.Transform = transformGroup;
thumbnail.EndInit();

// this will disconnect the stream from the image completely ...
WriteableBitmap writable = new WriteableBitmap(thumbnail);
writable.Freeze();
_imageSource = writable;
}

// Reading some metadata in a NON-UI thread throws an exception
// It's not as simple as doing a freeze, or cloning or ... anything
// else I could discover ... those all CRASHED.
Metadata = new Metadata(frame.Metadata as BitmapMetadata);
});
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex);
}
finally
{
if (mem != null)
{
mem.Close();
}
_loading = false;
RaisePropetyChanged("IsLoading");
RaisePropetyChanged("ImageSource");
}

}
}


First, the file is loaded into a MemoryStream. As I mentioned in an earlier post, I've found the fastest way to load a Bitmap in code is to avoid using a FileStream and the StreamSource property or the UriSource property directly. The best technique from my experiments is to load the file directly into a MemoryStream object and use that as the Stream parameter or property (depends on which Bitmap objects you're using).


 

      if (System.IO.File.Exists(_filename))
{
MemoryStream mem = null;
try
{
byte[] buffer = File.ReadAllBytes(_filename);
// by reading the data into an in-memory buffer, we prevent the file from being read in the UI thread
// -- which speeds up access dramatically!
mem = new MemoryStream(buffer);

 

The code verifies the file exists and then reads the entire file into a byte array and immediately creates a wrapping MemoryStream object.

Since I wanted to grab some of the Windows Imaging Component metadata, which isn't all threadsafe, I need to perform the next steps the main UI thread (I'll talk about why and what errors might happen if you don't later).

 

          // we need to read the metadata in the UI thread, so we'll open the bitmap and grab it using a 
          // delegate/invoke. (since the user is waiting for this, there's little benefit to doing this
          // as an async invoke).
          Application.Current.Dispatcher.Invoke(DispatcherPriority.Normal, (NoArgumentDelegate)delegate()
          {
            bool foundThumbnail;
            BitmapDecoder imgDecoder = BitmapDecoder.Create(mem, 
BitmapCreateOptions.None,
BitmapCacheOption.None); BitmapFrame frame = imgDecoder.Frames[0]; _imageSource = frame.Thumbnail; foundThumbnail = _imageSource != null;

Up until this point, I had created a background thread to load photos on demand from a queue -- so it was the background thread that fetched the data for the file.

Using the delegate syntax to make the code easier to debug/trace, I use the BitmapDecoder static method Create to open the file. Since this is a JPG file, there will only be one frame. The BitmapFrame object returned can be used directly as an image elsewhere, but in this case, I want something from that BitmapFrame, the Thumbnail.

Until I started working with these classes a few months ago, I hadn't realized that most modern digital cameras embed a thumbnail directly within the JPG file! Not only is that very cool, it's very practical as it's easy to access the data without needing to load and decode the entire image. That makes accessing the thumbnail fast. Very fast. I've got plenty of images that don't have a thumbnail however. Rather than maintaining an memory expensive large image and shrinking it on demand, I added code to create my own thumbnail on the fly.

 

              TransformedBitmap thumbnail = new TransformedBitmap();
              thumbnail.BeginInit();
              thumbnail.Source = frame as BitmapSource;
              
              // we'll make a reasonable sized thumnbail with a height of 240
              int pixelH = frame.PixelHeight;
              int pixelW = frame.PixelWidth;
              int decodeH = 240;
              int decodeW = (frame.PixelWidth * decodeH) / pixelH;
              double scaleX = decodeW / (double)pixelW;
              double scaleY = decodeH / (double)pixelH;
              TransformGroup transformGroup = new TransformGroup();
              transformGroup.Children.Add(new ScaleTransform(scaleX, scaleY));
              thumbnail.Transform = transformGroup;              
              thumbnail.EndInit();

With a little bit of math, a ScaleTransform, and using yet another media class, TransformedBitmap, I created a thumbnail, with a height of 240. Since I used the empty constructor, I needed to follow the ISupportInitialize pattern:  BeginInit and EndInit. Only within the context of a Begin/End can the properties of the TransformedBitmap class be set (well, at least only the generally useful properties). In the code above, it only sets the Transform property to the new TransformGroup.

When EndInit is called, the magic of the TransformedBitmap class happens (it's transformed!). The bitmap is now ready for use.

The thing that was bugging me though was the hard connection between the TransformedBitmap and the original BitmapFrame returned from the BitmapDecoder (and thus to the MemoryStream). Unless I could sever that connection, I couldn't close/dispose the MemoryStream. So, my simple work-around was to create a new image based on the transformed bitmap:

 

              // this will disconnect the stream from the image completely ...
              WriteableBitmap writable = new WriteableBitmap(thumbnail); 
              writable.Freeze();
              _imageSource = writable;

Bing. Done. I stored the new image away for use by a listbox (which is binding to a thumbnail property of my Photo class).

 

            // Reading some metadata in a NON-UI thread throws an exception
            // It's not as simple as doing a freeze, or cloning or ... anything
            // else I could discover ... those all CRASHED.
            Metadata = new Metadata(frame.Metadata as BitmapMetadata);

The final piece that I wanted to mention bugged me to no end! The BitmapMetadata classes -- they're great, but they aren't always thread safe. For certain types of metadata, they throw crazy exceptions.  This is my favorite:

A first chance exception of type 'System.Runtime.InteropServices.COMException' occurred in PresentationCore.dll
System.Runtime.InteropServices.COMException (0x800706F4): A null reference pointer was passed to the stub. (Exception from HRESULT: 0x800706F4)
at MS.Internal.HRESULT.Check(Int32 hr)
at System.Windows.Media.Imaging.BitmapMetadataEnumerator.MoveNext()
at PhotoScroll.Metadata.LoadKeywords(BitmapMetadata metadata) in d:\Dev\Source\PhotoScroll\PhotoScroll\Metadata.cs:line 187

Of course, the stub had a null reference pointer. (uhh?) If you ever mistakenly thought all this new great .NET 3.0 was all managed code -- BZZZT! Wrong. What was particularly frustrating about that exception is that it only happens with certain types of metadata queries! xmp appears to be the most likely to crash if it's not queried on the UI thread -- but I didn't do an exhaustive test by any means. So, my solution was, as I mentioned, to create the image and load the metadata on the UI thread. It affects overall performance of the application, but using the tricks I mentioned above, I've kept the performance to a very respectable level.

April 13, 2007

Photoscroll -- the worst named WPF demo application with source code!

With the exception of this project being called, WindowsApplication72, I'll admit that this is one of the poorest named projects I've distributed. I could have changed the name, but since it's really more of a demo right now than any truly useful functionality, I decided to not bother.

The intention of Photoscroll is to demonstrate a number of useful features in WPF -- and to show off some of the simple things that can be done with WPF that you may not have ever thought of including in an application if you weren't using WPF.

The application code written in C# is available for download below. You might be curious what the application looks like:

Cool animations when images are loading -- no 'jerky' or hanging user interface as the images load.

Tooltips on steroids -- with a Vista like glass effect (that works on XP).

Intermediate images shown while the full size image is loaded...

It uses the WIC BitmapMetadata (Windows Imaging Component) classes to extract available meta data from photos (and show it in a geeky, here's all the raw info available way):

If you have any questions about the code, feel free to send me an e-mail (coder@ this website).

Download (C#/.NET 3.0 required)

Due to the large size of the final EXE (as it contains quite a few graphics), you'll need to build the project to run it as I've excluded it from the download.

I'll be posting some details about the code very soon.

Here are some of the .NET 3.0 classes/properties that the sample code uses:

  • BitmapMetadata (ContainsQuery and GetQuery)
  • IValueConverters
  • Dispatcher
  • Binding
  • VirtualizingStackPanel
  • RoutedCommands
  • Table (and binding to a FlowDocument)
  • BitmapImage
  • BitmapDecoders and BitmapFrame
  • DoubleAnimation
  • DiscreteObjectKeyFrame
  • ControlTemplate
  • DataTrigger
  • Style
  • Trigger EnterAction ExitAction
  • Drag and Drop DataFormats.FileDrop
  • VisualBrush Visual
  • Thumbnail
  • Tooltip

Although I've put a copyright notice and warning on much of the code, all that I really want is a thank-you e-mail/post and a link to my website if you use the code. :)

April 9, 2007

Binding a table in a FlowDocument

Inspired by a post from Paul here providing a method to bind data to a FlowDocument Run object, I extended the idea to bind to a dynamic Table.  I prefer the attached property syntax in Paul's approach to a "BindableRun" object that Filipe used here. I didn't change the name from Paul's code (the name was fine for my purposes).

 

  public static class BindableExtender
  {
    public static Table GetBindableTable(DependencyObject obj)
    {
      return (Table)obj.GetValue(BindableTableProperty);
    }
    public static void SetBindableTable(DependencyObject obj, Table tbl)
    {
      obj.SetValue(BindableTableProperty, tbl);
    }
 
    public static string GetBindableText(DependencyObject obj)
    {
      return (string)obj.GetValue(BindableTextProperty);
    }
 
    public static void SetBindableText(DependencyObject obj,
        string value)
    {
      obj.SetValue(BindableTextProperty, value);
    }
 
    public static readonly DependencyProperty BindableTextProperty =
        DependencyProperty.RegisterAttached("BindableText" ,
            typeof(string) ,
                typeof(BindableExtender),
                new UIPropertyMetadata(null,
                BindableProperty_PropertyChanged));
 
    public static readonly DependencyProperty BindableTableProperty =
        DependencyProperty.RegisterAttached("BindableTable",
            typeof(Table),
                typeof(BindableExtender),
                new UIPropertyMetadata(null,
                BindableProperty_PropertyChanged));
 
    private static void BindableProperty_PropertyChanged(
        DependencyObject dependencyObject,
        DependencyPropertyChangedEventArgs e)
    {
      if (dependencyObject is Run)
      {
        ((Run)dependencyObject).Text = (string)e.NewValue;
      }
      if (dependencyObject is FlowDocument)
      {
        ((FlowDocument)dependencyObject).Blocks.Clear();
        if (e.NewValue != null)
        {
          ((FlowDocument)dependencyObject).Blocks.Add((Block)e.NewValue);
        }
      }
 
    }
  }

 

I added all of the code that handles Tables. Not too hard to find. The important stuff regarding binding happens in the PropertyChanged event. If the object passed is a FlowDocument, the code clears the current blocks that might be in the existing FlowDocument and then adds the Table.

How did I build the table? Using code like this to build the table:

 

private static Table BuildTable()
{
  Table tbl = new Table();
  TableRowGroup rowGrp = new TableRowGroup();
 
  tbl.RowGroups.Add(rowGrp);
 
  TableRow row = new TableRow();
  rowGrp.Rows.Add(row);
  // you can style a row ...
  // row.Style = Application.Current.TryFindResource("HeaderRowStyle") as Style;
  row.Cells.Add(new TableCell(new Paragraph(new Run("DataLabel:"))));
 
  Paragraph pg = new Paragraph();
  // or you can style a Paragraph
  // I couldn't get pg.SetResourceReference(Paragraph.StyleProperty, "DataStyle") to work
  // it seems to never locate the Style resource properly ... the other approach below works
  // however
  // pg.Style = Application.Current.TryFindResource("DataStyle") as Style;
  pg.Inlines.Add(new Run("Some data"));
  row.Cells.Add(pg);
 
  return tbl;
}

 

I added a property to the class bound by the UI:

 

    public Table Metadata
    {
      get { return BuildTable(); }
    }

 

Then, in the XAML, I bound to the property:

 

<FlowDocument local:BindableExtender.BindableTable="{Binding Path=Metadata}">
  <Paragraph/>
</FlowDocument>

 

The important thing to note here is the use of the attached property: local:BindableExtender.BindableTable.

The local namespace refers back to the host namespace (and since it's in the same assembly, the syntax just includes the namespace):

 

xmlns:local="clr-namespace:PhotoScroll"

 

 

 

Technorati tags: ,

April 6, 2007

Universal web application pattern vs. Experience first pattern

Ray Ozzie, Chief Software Architect at Microsoft, was interviewed by Knowledge@Wharton. It's an interesting interview -- broad with not too much depth, but he discusses the software and services question that comes up more and more these days -- how do I deliver my product, and to what platforms?

 

Ozzie:  The guidance that we are giving the development community -- and the guidance that we use in-house -- is to look at applications through the following lens: When the business model behind that app means that you have to get it everywhere, we call that the "universal web application pattern." When the most important thing is the experience that the user has with that application and you might be willing to trade off the breadth of the web for the richness of that experience, we call that an "experience first pattern."
There's no hard line between the two, but there is some guidance there. It's clear that the ad-based model is a "universal web pattern." The whole business model says, "Pick a technology for building that solution that gets to every eyeball on earth." At the opposite extreme are Windows games and, I believe, the Office Desktop components, which are "experience first." You want to make the experiences as rich as you can and you code to the [Windows] platform in order to do that.

 

Adobe is a great example. Flash is a rich client; it's rich code delivered to the edge. It's not a centralized model; it's a decentralized model. It just happens to be tethered to the service.
If anybody has a software and services model, it's Adobe, because of that rich [Flash Player] applet that they extend the browser with. The more they enhance that, as you can see in their Flex and Apollo plans, the more it becomes this unified software and service vision, which is basically the same as Microsoft's vision.
We've been talking about it mostly from a technology perspective and what that can do for an end user. One of the other big things that [a] services [architecture] does for traditional software companies is change the way that we touch our customers.

Long, but interesting if you have the time.

Help support my web site by searching and buying through Amazon.com (in assocation with Amazon.com).