|
|
June 24th, 2009
Please check you mailbox for the latest issue of CoDe Magazine. In it, you will find my article "INotifyPropertyChanged is Obsolete." A few things have changed since I penned those words, and now INotifyPropertyChanged is more obsolete than ever.
The article describes how to use Update Controls to replace WPF data binding. Rather than using the built-in "{Binding}" XAML extension, it tells you to use my "{u:Update}" custom extension. While this custom markup extension is still supported, it is no longer the preferred way to use Update Controls.
Blend-friendly updating
Thanks to some feedback from Paul Stovell, Microsoft MVP for Client Application Development, Update Controls now works with the "{Binding}" XAML extension. This makes it more Blend friendly. But before you give your object to the DataContext, you need to wrap it:
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
DataContext = ForView.Wrap(new PersonViewModel(new Person()));
}
}
The wrapper implements INotifyPropertyChanged for you. It infers dependencies from your code and figures out when to fire PropertyChanged events. To help it with that inference, you need to mark your independent properties.
public class Person
{
private string _firstName;
private string _lastName;
#region Independent properties
// Generated by Update Controls --------------------------------
private Independent _indFirstName = new Independent();
private Independent _indLastName = new Independent();
public string FirstName
{
get { _indFirstName.OnGet(); return _firstName; }
set { _indFirstName.OnSet(); _firstName = value; }
}
public string LastName
{
get { _indLastName.OnGet(); return _lastName; }
set { _indLastName.OnSet(); _lastName = value; }
}
// End generated code --------------------------------
#endregion
public string FullName
{
get
{
return FirstName + " " + LastName;
}
}
}
The Ctrl+D, G shortcut still works. Select the fields and hit the shortcut to generate the independent properties.
Update Controls can see right through the intermediate view model. There is no bookkeeping code required. Even the dependency of Title upon FirstName and LastName is inferred.
public class PersonViewModel
{
private Person _person;
public PersonViewModel(Person person)
{
_person = person;
}
public string FirstName
{
get { return _person.FirstName; }
set { _person.FirstName = value; }
}
public string LastName
{
get { return _person.LastName; }
set { _person.LastName = value; }
}
public string FullName
{
get { return _person.FullName; }
}
public string Title
{
get { return "Person - " + FullName; }
}
}
Silverlight 3
I have also ported Update Controls to Silverlight. It only works with Silverlight 3, so it is in Beta until the official release. However, it is fully operational and works just as well as the WPF version. The "{u:Update}" custom extension is not supported in Silverlight, however. You have to use the new ForView.Wrap() mechanism.
I hope you find Update Controls to be as useful as I have. You can download the source code provided in the article from CoDe Magazine, or try some demos that I've created since then:
- Commuter - Multi-threaded iTunes synchronization sample.
- QuickWriter - Data binding through linq queries. Uses the {u:Update} extension rather than the new ForView.Wrap().
- UpdateControls.XAML.Test - The test app bundled with the source code.
- Addendum Demo - The code included above.
Also be sure to watch the videos to see it in action.
Posted in Update Controls | No Comments »
June 13th, 2009
My wife just took some new photos for her scrapbooking website. I have a coverflow effect on that page, so I needed to apply the wet floor transform to 64 images. Rather than do it all by hand, I wrote some WPF code to do it for me:
public class WetFloor
{
public static void Render(string source, string target, int targetWidth)
{
using (Stream input = File.OpenRead(source))
{
// Load the source jpg.
JpegBitmapDecoder sourceJpg = new JpegBitmapDecoder(input, BitmapCreateOptions.None, BitmapCacheOption.Default);
if (sourceJpg.Frames.Count != 1)
throw new Exception(String.Format("I don't know how to handle {0} frames.", sourceJpg.Frames.Count));
BitmapFrame sourceBitmap = sourceJpg.Frames[0];
// Create a render target scaled to 400 pixels at 150% the aspect ratio.
double scale = (double)targetWidth / sourceBitmap.PixelWidth;
RenderTargetBitmap renderTarget = new RenderTargetBitmap(
targetWidth, (int)Math.Round(scale * (float)sourceBitmap.PixelHeight * 1.5),
sourceBitmap.DpiX, sourceBitmap.DpiY,
PixelFormats.Pbgra32);
// Paint a rectangle with the source bitmap.
Rectangle original = new Rectangle()
{
Fill = new ImageBrush(sourceBitmap),
Width = scale * sourceBitmap.Width,
Height = scale * sourceBitmap.Height
};
original.Arrange(new Rect(new Size(scale * sourceBitmap.Width, scale * sourceBitmap.Height)));
renderTarget.Render(original);
// Paint a black rectangle.
Rectangle shade = new Rectangle()
{
Fill = new SolidColorBrush(Colors.Black),
Width = scale * sourceBitmap.Width,
Height = scale * sourceBitmap.Height
};
shade.Arrange(new Rect(new Point(0, scale * sourceBitmap.Height), new Size(scale * sourceBitmap.Width, scale * sourceBitmap.Height)));
renderTarget.Render(shade);
// Paint an inverted rectangle with the source bitmap.
Border reflection = new Border()
{
Background = new ImageBrush(sourceBitmap),
Width = scale * sourceBitmap.Width,
Height = scale * sourceBitmap.Height,
RenderTransform = new ScaleTransform(1.0f, -1.0f, scale * sourceBitmap.Width / 2, scale * sourceBitmap.Height / 2),
OpacityMask = new LinearGradientBrush()
{
StartPoint = new Point(0, 1),
EndPoint = new Point(0, 0),
GradientStops = new GradientStopCollection(new GradientStop[]
{
new GradientStop()
{
Offset = -0.3f,
Color = Colors.Black
},
new GradientStop()
{
Offset = 0.5f,
Color = Colors.Transparent
}
})
}
};
reflection.Arrange(new Rect(new Point(0, scale * sourceBitmap.Height), new Size(scale * sourceBitmap.Width, scale * sourceBitmap.Height)));
renderTarget.Render(reflection);
// Render the final product.
JpegBitmapEncoder png = new JpegBitmapEncoder();
png.Frames.Add(BitmapFrame.Create(renderTarget));
using (Stream fs = File.Create(target))
{
png.Save(fs);
}
}
}
}
Enjoy.
Posted in Uncategorized | No Comments »
June 10th, 2009
Multithreaded programming doesn't have to be hard. You just need to learn a few patterns.
Here's a common problem. How do you notify the UI thread that something has changed on a background thread? The INotifyPropertyChanged contract requires that the PropertyChanged event be fired on the UI thread. There are ways of marshalling that event from the background to the foreground, but wouldn't it be nice if you didn't have to?
Update Controls takes care of this problem for you. You make sure your code is thread-safe, and Update Controls will make sure that dependents are updated, no matter what thread they are on.
A thread-safe model
For Update Controls to work properly -- and indeed for your entire application to work properly -- you need your data model to be thread safe. That means that anything that can change is protected with a lock. If it can change (and you are using Update Controls), then it should have an Independent sentry. Just be sure to call OnGet and OnSet inside the lock.
public class Playlist
{
private int _id;
private string _name;
public Playlist(int id)
{
_id = id;
}
public int ID
{
get { return _id; }
}
#region Independent properties
// Generated by Update Controls --------------------------------
private Independent _indName = new Independent();
public string Name
{
get
{
lock (this)
{
_indName.OnGet();
return _name;
}
}
set
{
lock (this)
{
_indName.OnSet();
_name = value;
}
}
}
// End generated code --------------------------------
#endregion
}
When dealing with collections, we need to be a little careful. If you return an IEnumerable that traverses a collection, that IEnumerable will leave the lock. This will cause problems, as another thread can come in and modify the collection while you are traversing it. To be safe, we need to create a copy of the collection within the lock.
public class MusicLibrary
{
private List<Playlist> _playlists = new List<Playlist>();
#region Independent properties
// Generated by Update Controls --------------------------------
private Independent _indPlaylists = new Independent();
public Playlist GetPlaylist(int playlistID)
{
lock (this)
{
// This method changes the list.
_indPlaylists.OnSet();
// See if we already have this one.
Playlist playlist = _playlists.Where(p => p.ID == playlistID).FirstOrDefault();
if (playlist != null)
return playlist;
// If not, create it.
playlist = new Playlist(playlistID);
_playlists.Add(playlist);
return playlist;
}
}
public void DeletePlaylist(Playlist playlist)
{
lock (this)
{
_indPlaylists.OnSet();
_playlists.Remove(playlist);
}
}
public IEnumerable<Playlist> Playlists
{
get
{
lock (this)
{
_indPlaylists.OnGet();
// Return a copy of the list so that it can be accessed in a
// thread-safe manner.
return new List<Playlist>(_playlists);
}
}
}
// End generated code --------------------------------
#endregion
}
With this, your model is safe to access from multiple threads. So let's create one.
Create a thread to communicate with external systems
The UI thread is for the user. If you use it to communicate with external systems, it might hang. Then the user will have to decide whether to "Continue Waiting" or "Switch To...". Switch to what, a Mac? But seriously, it's better if you spawn a new thread to do all of your communication with external systems.
Commuter communicates with iTunes. So I created a separate thread to synchronize between iTunes and my data model. It wakes up every 10 seconds and refreshes the playlists.
public class ITunesSynchronizationService
{
private MusicLibrary _musicLibrary;
private Thread _thread;
private ManualResetEvent _stop;
private bool _first = true;
private string _lastError = string.Empty;
private Independent _indLastError = new Independent();
public ITunesSynchronizationService(MusicLibrary musicLibrary)
{
_musicLibrary = musicLibrary;
_thread = new Thread(ThreadProc);
_thread.Name = "iTunes synchronization service";
_stop = new ManualResetEvent(false);
}
public void Start()
{
_thread.Start();
}
public void Stop()
{
_stop.Set();
// Give it 30 seconds to shut down.
_thread.Join(30000);
}
public string LastError
{
get
{
lock (this)
{
_indLastError.OnGet();
return _lastError;
}
}
private set
{
lock (this)
{
_indLastError.OnSet();
_lastError = value;
}
}
}
private void ThreadProc()
{
while (ShouldContinue())
{
try
{
// Connect to iTunes.
iTunesApp itApp = new iTunesAppClass();
// List all of the playlists.
List<Playlist> oldPlaylists = _musicLibrary.Playlists.ToList();
foreach (IITUserPlaylist itPlaylist in itApp.LibrarySource.Playlists.OfType<IITUserPlaylist>())
{
string name = itPlaylist.Name;
Playlist playlist = _musicLibrary.GetPlaylist(itPlaylist.playlistID);
if (name != null && playlist.Name != name)
playlist.Name = name;
oldPlaylists.Remove(playlist);
}
// Delete all that are no longer in iTunes.
foreach (Playlist playlist in oldPlaylists)
_musicLibrary.DeletePlaylist(playlist);
LastError = string.Empty;
}
catch (Exception ex)
{
LastError = ex.Message;
}
}
}
private bool ShouldContinue()
{
// Don't wait the first time.
if (_first)
{
_first = false;
return true;
}
// Wake up every 10 seconds, or when the thread should stop.
return !_stop.WaitOne(10000);
}
}
Notice the LastError property. This is a thread-safe way for the service to communicate its status with the user. This property is bound to a TextBlock on the UI.
The iTunes synchronization service makes changes directly to the data model. Because the data model is thread-safe, the UI thread and the background thread can share this resource. Update Controls will notify the UI thread whenever the background thread changes the data model.
Try it out
Download the code. When you run Commuter, it will launch iTunes. Even as iTunes launches, the Commuter user interface is responsive. If you are fast enough, you can open the Playlist drop-down before it is populated. But even if COM is faster than the mouse, you can try a few experiments.
Select a playlist in Commuter. Then switch to iTunes and rename that playlist. Within 10 seconds you'll see the selected playlist's name change.
Bring iTunes to the front, but leave Commuter visible underneath it. Place your mouse on the drop-down. Press Ctrl+N to create a new playlist, then click the mouse to bring Commuter to the front and open the drop-down. Within 10 seconds, "untitled playlist" will appear in the list.
Multi-threaded programming still requires some care, but Update Controls can at least take care of updating the UI when the background thread changes the data model.
Posted in Commuter, Update Controls | No Comments »
June 9th, 2009
Historic Modeling may be a new concept, but you are probably already familiar with a popular example.
A person is a fact. The person exists. Time may pass, and perhaps she'll die, but the fact that she existed will always be true.

The same is true of an animal.

A person might consume an animal. Usually the reason is sustenance, but we can't be sure. We can document the fact that a person swallowed an animal for a reason.

Any of a number of reasons can be given. Maybe we just don't know. Or maybe the person swallowed the animal to catch an animal that they had previously swallowed. The fact that they swallowed an animal in the past is a prerequisite for the reason to swallow another. The prior swallow must come before the reason.

From model to instance
To demonstrate the model, let's create some instances of these facts.
- There was an old lady.
- There was a fly.
- There was an old lady who swallowed a fly. I don't know why she swallowed the fly.
- There was an old lady who swallowed a spider. She swallowed the spider to catch the fly.
Continuing in this manner, we construct a graph of fact instances.

This recitation of facts could go on. Until she dies, of course.
Posted in Fun, Historic Modeling | No Comments »
June 5th, 2009
The Commuter View Model interface is written. The next step is to hook it to a view.
I like to think of the view as just a convenience. If the user could access properties directly, all they would need is the View Model interface. Thinking about the View Model this way helps me ensure that it is complete and targeted.
So I created the interface as a way of describing what would be on the view. A designer given the interface and a wireframe could build a view. Not having a designer handy, I did it myself. Here's how.
Create a mock The first step was to create a mock implementation of the View Model interface. In truth, it's not just one interface; it's a set of interrelated interfaces. So I have interrelated mocks. Most of them are simple data access objects: collections of properties with no behavior. Create a read/write property for every property of the interface, even if the interface property is read-only. So for the IPodcastEpisode interface:
public interface IPodcastEpisode
{
string Name { get; }
string PodcastName { get; }
string DurationAsString { get; }
}
I created a PodcastEpisodeSample class:
public class PodcastEpisodeSample : IPodcastEpisode
{
public string Name { get; set; }
public string PodcastName { get; set; }
public string DurationAsString { get; set; }
}
The root class has a little more to it. It has to initialize itself and its children in its constructor. I don't like putting code in a constructor, but you don't have another chance to set your properties.
public class CommuterViewModelSample : ICommuterViewModel
{
public CommuterViewModelSample()
{
// Set up some sample data.
List<IPlaylist> playlists = new List<IPlaylist>();
playlists.Add(new PlaylistSample() { Name = "Favorites" });
PlaylistSample commutePlaylist = new PlaylistSample() { Name = "Commute" };
playlists.Add(commutePlaylist);
playlists.Add(new PlaylistSample() { Name = "Energy Songs" });
Playlists = playlists;
SelectedPlaylist = commutePlaylist;
List<IPodcast> podcasts = new List<IPodcast>();
podcasts.Add(new PodcastViewModel(new Podcast() { Name = ".NET Rocks!", Selected = false, Rank = 0 }));
podcasts.Add(new PodcastViewModel(new Podcast() { Name = "Hanselminutes", Selected = true, Rank = 3 }));
podcasts.Add(new PodcastViewModel(new Podcast() { Name = "Deep Fried Bytes", Selected = true, Rank = 4 }));
podcasts.Add(new PodcastViewModel(new Podcast() { Name = "They Might Be Giants", Selected = true, Rank = 5 }));
Podcasts = podcasts;
List<IPodcastEpisode> queue = new List<IPodcastEpisode>();
queue.Add(new PodcastEpisodeSample() { Name = "5-A", PodcastName = "They Might Be Giants", DurationAsString = "9:30" });
queue.Add(new PodcastEpisodeSample() { Name = "JavaFX and the Web's Four Virtual Machines", PodcastName = "Hanselminutes", DurationAsString = "40:24" });
PodcastEpisodeSample deepFriedBytesEpisode = new PodcastEpisodeSample() { Name = "New Ideas for the Web with Thomas Krotkiewski", PodcastName = "Deep Fried Bytes", DurationAsString = "53:15" };
queue.Add(deepFriedBytesEpisode);
Queue = queue;
SelectedEpisode = deepFriedBytesEpisode;
TotalDurationAsString = "1:43:09";
}
public IEnumerable<IPlaylist> Playlists { get; set; }
public IPlaylist SelectedPlaylist { get; set; }
public IEnumerable<IPodcast> Podcasts { get; set; }
public IEnumerable<IPodcastEpisode> Queue { get; set; }
public IPodcastEpisode SelectedEpisode { get; set; }
public string TotalDurationAsString { get; set; }
public ICommand Skip { get; set; }
}
You can hook up the mock in Blend. In the Window's properties, press the "New" button next to DataContext. Select the CommuterViewModelSample class from the solution. Now Blend will show the data from your mock object while you edit the view.
Overall structure: Grid The structure of the Window itself is defined by a Grid. Grid is the best container for a window, because it supports "star" sizing. You can be specific about the size of some components, and then leave the rest of the real estate to be carved up proportionally. A Grid is the default layout root that Blend puts into a window, but if you have something else there (like a StackPanel for example), you can right-click on it and select "Change Layout Type".
Select the Grid in the object pane (where you right-clicked to change the layout type) and expand the layout properties. Hit the ellipsis button to set the RowDefinitions property. For Commuter I added 8 rows. I set the height of all but one to "Auto"; the remaining one I left at "1 Star". This one row will resize with the windows, and the others will be the height of their contents.
I populated the rows with controls, alternating between TextBlocks for labels and other controls for inputs. To tell you the truth, it was easier to drop into XML for this part. If you've set the row sizes to "Auto", then there is no space into which to drop controls. And if you haven't you spend a lot of time resetting Margin, HorizontalAlignment, and VerticalAlignment after you drag a control into the right row. Here's the structure so far.
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Commuter.Window1"
Title="Commuter" Height="420" Width="540" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" xmlns:Commuter="clr-namespace:Commuter">
<Window.DataContext>
<Commuter:CommuterViewModelSample/>
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
</Grid>
</Window>
Non-resizeable list: ListBox of Grid The Podcasts list contains for each podcast a selection box, rating buttons, and the name of the podcast. The selection box and rating buttons are all fixed width. The only thing that varies is the width of the name. This sounds like a job for Grid again. The buttons can assume the width of their contents ("Auto"), while the name can consume all remaining space ("1 Star").
I dropped a ListBox into the appropriate row and set its Height property to a number I liked. Dragging the handle didn't do the right thing, because Blend couldn't tell whether I wanted to resize the row or not. When the property is set explicitly, it's obvious what you intend.
The next step was to bind the ItemsSource to the Podcasts property of the View Model. Click on the little gray dot to the right of the ItemsSource property and select "Data Binding". You'll want the "Explicit Data Context" tab, though I'm not sure why it's called that. You're using the DataContext inherited from the parent Window. Open up the View Model object and select the collection you want to show. I chose the Podcasts property.
Now you need to define how each item in the list looks. Right-click on the ListBox and select "Edit Control Parts (Template)", "Create Empty". Give the template an appropriate name (like PodcastsTemplate), leave "This Window" selected, and hit "OK". This creates a data template resource within the Window itself. If you had created it in the Application, it could be shared among windows. Since I don't see a need for that, I kept it local.
I'm not sure why this is called a "data template". It seems that it should be called a "view template". It's not a template for the data: that's defined by the bound interface property. Anyway...
From here, you can use a Grid layout just like you did for the Window, but set the ColumnDefinitions instead of the RowDefinitions. When you're done, click the button in the picture on the right to go back to the window. I can't tell you how long it took me to discover this thing.
You may notice that each of the rows is squashed at this point. Set the HorizontalContentAlignment property to "Stretch" to fix this.
Here's the resulting XAML.
<DataTemplate x:Key="PodcastsTemplate">
<Grid Height="18">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="38"/>
<ColumnDefinition Width="38"/>
<ColumnDefinition Width="38"/>
<ColumnDefinition Width="38"/>
<ColumnDefinition Width="38"/>
</Grid.ColumnDefinitions>
<CheckBox Width="13" VerticalAlignment="Center" Grid.Column="0"/>
<TextBlock Width="Auto" Text="{Binding Path=Name, Mode=Default}" Grid.Column="1" VerticalAlignment="Center"/>
<CheckBox Grid.Column="2" Template="{DynamicResource CarCheckBox}" IsChecked="{Binding Path=Rank1, Mode=Default}"/>
<CheckBox Grid.Column="3" Template="{DynamicResource CarCheckBox}" IsChecked="{Binding Path=Rank2, Mode=Default}"/>
<CheckBox Grid.Column="4" Template="{DynamicResource CarCheckBox}" IsChecked="{Binding Path=Rank3, Mode=Default}"/>
<CheckBox Grid.Column="5" Template="{DynamicResource CarCheckBox}" IsChecked="{Binding Path=Rank4, Mode=Default}"/>
<CheckBox Grid.Column="6" Template="{DynamicResource CarCheckBox}" IsChecked="{Binding Path=Rank5, Mode=Default}"/>
</Grid>
</DataTemplate>
...
<ListBox ItemsSource="{Binding Path=Podcasts, Mode=Default}" ItemTemplate="{DynamicResource PodcastsTemplate}" HorizontalContentAlignment="Stretch" Height="76" Grid.Row="3"/>
Resizable list: ListView The list of episodes contains three string columns. It makes sense to let the user individually resize these, unlike the single text field among a bunch of fixed-width buttons. So I decided on a ListView for this control.
You'll probably have to click the chevrons on the bottom of the toolbar to find the ListView tool. It's not one that's visible out-of-the-box. Set the ItemsSource just as before, But this time, rather than defining a data template, you need to set the View. Expand the "View" property hiding in "Miscellaneous". Click on the "Columns" button to add three columns. Set the Header property and bind the DisplayMemberBinding.
Here's one frustrating part of Blend. Even though we've bound the ItemsSource, Blend presents us with the properties of the CommuterViewModelSample. It should be able to recognize that the ItemsSource is a collection of IPodcastEpisode and give us the properties of that interface. Since it can't we just have to manually enter a path expression. Check the box at the bottom and enter the property name.
<ListView ItemsSource="{Binding Path=Queue, Mode=Default}" SelectedItem="{Binding Path=SelectedEpisode, Mode=Default}" Grid.Row="5" HorizontalContentAlignment="Stretch">
<ListView.View>
<GridView>
<GridViewColumn Width="300" Header="Name" DisplayMemberBinding="{Binding Name}"/>
<GridViewColumn Width="140" Header="Podcast" DisplayMemberBinding="{Binding PodcastName}"/>
<GridViewColumn Width="50" Header="Duration" DisplayMemberBinding="{Binding DurationAsString}"/>
</GridView>
</ListView.View>
</ListView>
Spacing: Style By default, all of the controls in a grid are pushed right up next to each other. To add spacing, I defined a set of styles. Each style targets a control type in the grid, and sets the Margin property.
<Window.Resources>
<Style TargetType="{x:Type ComboBox}">
<Setter Property="Margin" Value="3"/>
</Style>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Margin" Value="3"/>
</Style>
<Style TargetType="{x:Type ListBox}">
<Setter Property="Margin" Value="3"/>
</Style>
<Style TargetType="{x:Type ListView}">
<Setter Property="Margin" Value="3"/>
</Style>
<Style TargetType="{x:Type Button}">
<Setter Property="Margin" Value="3"/>
</Style>
</Window.Resources>
Going back to Visual Studio These patterns covered what I needed for this simple view. I suspect that they will cover most of my needs for data entry windows.
Overall, I find Blend to be a bit cumbersome to work in. It was great for the geometry of the car buttons, but it gets in the way when laying out a form. Dropping a control in the right place is not possible when the container size is Auto. Several extra properties get set on your behalf, which you then need to hunt down and reset. And even though the XAML that I need is small, the Blend UI hides many of the properties behind expanders, buttons, and tabs. I have to navigate several pages just to see what is immediately visible in a single line of XAML.
As a XAML editor, Blend is poor. It offers no intellisense the way that Visual Studio does. So now that I know the XAML that I need, I expect I'll be spending a lot more time in Visual Studio hand-crafting XAML.
Posted in Commuter, User Interface | 1 Comment »
May 30th, 2009
The Big (D)esign Conference was an upbeat collaboration among designers, developers, and strategists. The common theme was finding ways to improve the quality of peoples lives through software. That's a theme that resonates with me, as I have made that my personal and professional mission.
This was a brand new conference. As such, there were some minor logistical problems, like a scarcity of water. But the attendees took to Twitter and the organizers responded. By the end of lunch, water bottles were abundant.
That's just one example of the dynamic nature of this conference. It wasn't about people expecting a solution to be handed to them. It was about people finding solutions, responding to needs, and making the conference a success. The organizers did a great job, but it was the attitude of the attendees -- who really made the conference their own -- that made this day a success.
The Twitter hash tag was in full force during the conference. Many attendees Twittered salient points of the presentations, their own opinions of the topics, or just their sandwich selection. The Twitter stream was as lively a conversation as any occurring in the hall. Through this conversation, I made several new friends.
In addition to making new friends, I ran into some old ones. One of my old friends, T. Scott, is a designer. He recorded his notes and uploaded them for all to enjoy. So I decided to do the same, in my own way. My writing is not so beautiful, so here is a transcription of my notes from the sessions I attended.
Keynote: Norm Cox, Interaction Therapist Titles are limiting. When we think of ourselves as developer, designer, business analyst, or architect, we tell ourselves (and others) that that is all we do. We limit our own thinking.
We should instead become jacks of many trades, masters of some. Know all of the disciplines related to solving someone's problem. Know which ones you are particularly good at, but don't let your skill set limit your solution.
Clients don't know what they need. Don't do exactly as they tell you to. Be a therapist. See the big picture. Find the real problem. Don't limit yourself to the client's expectations.
Chris Koenig: Touch Computing A Natural User Interface (NUI) is touch, gesture, manipulation, and more. It doesn't replace the keyboard or the mouse. It is designed for different scenarios. And sometimes, it augments them.
NUI is good for free-form exploration and social interaction. It is not good for precision. It is not good for data density. Those are best left to the keyboard and mouse.
Touch is not click. Don't take old metaphors and idioms and apply them to NUI. This is a new space, with a new set of benefits. Add it to your tool belt, but don't replace your trusted tools.
Stephen P Anderson: The Art and Science of Seductive Interactions Good usability lowers friction. But that's not enough. You also need to increase motivation. That can only be achieved by understanding psychology.
People respond to levels and rewards. LinkedIn rates the completeness of your profile, and tells you what to do to take it to the next level. That motivates people to behave the way LinkedIn wants. This behavior benefits LinkedIn, but it also benefits the individual.
Seduction is deliberately enticing a person to perform a desired behavior. It requires feedback. The person needs to see that their actions modify the results. Their curiosity will lead them to try other behaviors and see how the results change.
People respond to visual imagery. They look for patterns in everything they see. They recognize, even if they can't recall. Software that uses these traits to its advantage will successfully guide users to perform the desired behavior. So show people images and have them find patterns that give you information about them. Don't give them a blank slate and ask them to recall that information.
Bill Scott, design patterns you should know Nuance is where a lot of good interaction is missed. Pay attention to the details to eliminate friction.
Make it direct. Where there is output, let there also be input. Let interactions be symmetric; don't enter a feature one way and leave it another.
Make it lightweight. Interactions have a "click-weight". Too many interactions, and your users are going to get tired and go away.
Avoid confusing and annoying anti-patterns. Don't hover-and-cover, popping up new content over old content, or hiding navigation. Avoid "mystery meat" navigation, when the user doesn't know what a click is going to do until they do it. Follow Fitts' Law and give the users a big enough target.
Stay on the page. Use overlays (pop-out) and inlays (slide-down) wisely. Don't stop the proceedings with idiocy. Use carousels, endless scrolling, and deep zoom where appropriate.
Offer an invitation. Play on your user's curiosity. Give them a call to action. Show them the steps involved. Walk them through. Make it discoverable.
Show transitions. Fade in and out. Animate zoom and motion to draw people to the next state.
Garrett Dimon, Feedback and Feature Requests The creator of Sifter talks about how he solicits feedback from his customers, and how he rolls that in to new features of the product.
The lifecycle of feedback is: generate, receive, reply, absorb, and respond. Generate feedback by actively soliciting it from your users. Receive it in the form of analytics, web forms, email, phone calls, and anything else that is accessible to your users. Reply quickly to tell them whether you're looking into it, or if not why. Take the time to absorb the feature request and consider its full impact. Then respond by implementing the feature, a different feature, or giving the customer the results of your full consideration.
Don't be afraid to be transparent in your product development process. People will not steal your ideas. If your idea is really great, you are going to have to shove it down their throats.
If it needs instructions, then it's broken. Instead of explaining to people how to use your software, fix the problem. If you have Frequently Asked Questions, you have feedback. Fix it.
The proportion of criticism to encouragement is absurd. It will bring you down. But always be upbeat when you reply. It will turn the conversation around and open a constructive dialog.
Always think about the need, not the prescription. Find out what problem people have. Don't give them the solution they ask for. Don't abdicate software design to the users. Listen to them, then deliver a solution.
There are no takebacks with features. Consider whether it really belongs in your product before adding it. It may delight some people but confuse others. If you add it, you will have to support it for the life of the product.
Todd Wilkens, Saying "No" and Failing Saying "No" limits the scope and gives you clear focus. First evaluate features based on importance and feasibility. Implement the ones that are important and feasible. Consider the ones in the middle. Ignore the ones at the low end of the scales.
Design is about specific customer value. Start with the things that you can do exceedingly well, and deliver that value completely. Each evolutionary step after that should provide complete customer value for a new problem, not a new part of an incomplete solution.
Failure is always an option. Everybody fails often. Just make sure you are failing forward. Fail early and often, and learn from each iteration.
If failure is not allowed, then risk is not allowed. The companies that succeed openly encourage failure internally. Separate personal feelings from ideas, so that the ideas can fail without damaging the people.
Have a conversation with the materials. Through the process of making something, we change the thing that we are making in response to the stuff that we are making it out of. Create an informal iterative process for failing often.
Thor Muller, Work Like the Network Challenge the assumption of industry: that it is expensive to coordinate a large number of people to build something. When we move electrons instead of molecules, those economics no longer hold. The use of a physical product reduces its value. The use of an idea increases its value.
There are six ways to remodel the organization to act more like a network.
| Move from |
To |
| Control |
Chaos |
| Process |
Flow |
| Documentation |
Collaboration |
| Fear |
Confidence |
| Hidden |
Shown |
| Ownership |
Stewardship |
It was a relief to hear someone talk about agile processes and flow without being dogmatic about it. Thor just demonstrated the value of these principles, and showed how organizations have applied them successfully.
Christina Wasson, When Design Met Anthropology Ethnographic anthropology is the study of cultures by becoming an observant participant. It is a systematic approach involving capturing information, coding it, and analyzing it. These techniques can be applied to software to give us a scientific basis for measuring the quality of a design.
Ironically, the case study that Christina shared was juxtaposed against the spirit of the conference. a study of Social TV, a Motorola project to join families in different living rooms around the viewing experience. The case study was punctuated by videos of people talking about how they negotiate over the remote, how television fits in with their lives, and how their family members react. It was depressing to see people still engaging in a one-sided conversation with old media, instead of using their talents to create something awesome.
I'm excited to have met such a fantastic group of people. I look forward to working with them to improve the quality of peoples lives through software. I'm looking forward to checking in next year to see how we've done.
Posted in User Interface | No Comments »
May 28th, 2009
Here's a perfect example of the wrong way to defend against SQL injection attacks.
I am trying to obtain a license for NDepend for use in my open source project Update Controls. Their download form asks for an email address. So I provided the email address I use for that project. The form didn't like it:

XKCD warns us to "sanitize our database inputs". That is completely wrong. Honor the user's input, but treat it as text, not code.
Posted in Databases | 2 Comments »
May 28th, 2009
While researching for Commuter, I found several implementations of star-rating controls on the web. Here are a few:
There's some good work here, so give them a read. However, a common theme was to control the stars with code. I wanted to do a purely View Model driven design to limit the amount of code necessary.
The View Model is dependent behavior
The first thing to realize is that a star rating control is a combination of independent and dependent behavior. While the rating itself is independent, the state of each star is dependent. The independent data lives in the model, and the dependent data lives in the View Model. So I changed the podcast View Model contract to this:
public interface IPodcast
{
string Name { get; }
bool Selected { get; set; }
bool Rank1 { get; set; }
bool Rank2 { get; set; }
bool Rank3 { get; set; }
bool Rank4 { get; set; }
bool Rank5 { get; set; }
}
This lets me just drop five ordinary check boxes and bind them to the five properties. Well, not ordinary check boxes. Mine look like cars. Here's a step-by-step guide for styling a control. Use the same technique to create star checkboxes, if you are so inclined.
Implement the View Model contract A star -- er a car -- is on if the rating is greater than or equal to its value. When a car is clicked, it sets the rank to its value. This logic is encoded in the View Model like this:
public class PodcastViewModel : IPodcast
{
private Podcast _model;
public PodcastViewModel(Podcast model)
{
_model = model;
}
public string Name
{
get { return _model.Name; }
set { _model.Name = value; }
}
public bool Selected
{
get { return _model.Selected; }
set { _model.Selected = value; }
}
public bool Rank1
{
get { return _model.Rank >= 1; }
set { _model.Rank = 1; }
}
public bool Rank2
{
get { return _model.Rank >= 2; }
set { _model.Rank = 2; }
}
public bool Rank3
{
get { return _model.Rank >= 3; }
set { _model.Rank = 3; }
}
public bool Rank4
{
get { return _model.Rank >= 4; }
set { _model.Rank = 4; }
}
public bool Rank5
{
get { return _model.Rank >= 5; }
set { _model.Rank = 5; }
}
}
The View Model does not maintain any state of its own. It delegates everything to the model. It is just a translator that converts model values into view behaviors.
Implement INotifyPropertyChanged Oh, wait! You don't need to do that. Just use Independent sentries in your data model:
public class Podcast
{
private string _name = string.Empty;
private bool _selected = false;
private int _rank = 1;
#region Independent properties
// Generated by Update Controls --------------------------------
private Independent _indName = new Independent();
private Independent _indSelected = new Independent();
private Independent _indRank = new Independent();
public string Name
{
get { _indName.OnGet(); return _name; }
set { _indName.OnSet(); _name = value; }
}
public bool Selected
{
get { _indSelected.OnGet(); return _selected; }
set { _indSelected.OnSet(); _selected = value; }
}
public int Rank
{
get { _indRank.OnGet(); return _rank; }
set { _indRank.OnSet(); _rank = value; }
}
// End generated code --------------------------------
#endregion
}
And then wrap the view model for the view (requires Update Controls release 2.0.4.0 or better):
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
DataContext = ForView.Wrap(new CommuterViewModel(new CommuterModel()));
}
}
Now when you click on any of the cars, it will set the Rank property in the data model. Since all of the check boxes depend upon that one Rank, they are all updated accordingly.
Posted in Commuter, Update Controls | No Comments »
May 26th, 2009
I just requested a change to a use case that will save the project two man-weeks.
eCommerce users and accounts In the ABSG eCommerce system, a user shops on behalf of an account. Some users can shop on behalf of more than one account, so they get to choose at the beginning of their shopping experience. We populate their list of choices from all of the accounts to which they have access.
Employees of ABSG log on to the eCommerce system to help people. Some of these employees are sales representatives for certain accounts. Every account as one sales rep.
Not all users are employees, and not all employees are users. We enter an employee ID in the user profile to relate the two.
The UserAccount table
We have an associative table that relates users with the accounts that they have access to. This is the table that we use to present the list of choices at the beginning of the user's shopping experience.
I'm sure at one point the BA asked me how a user is granted access to an account. I told her about the UserAccount table. It was probably that conversation that led to the Automatic Account Access use case.
Automatic account access Use case 1019: Automatic Account Access stated that the system shall periodically check for changes:
- An account is created or deleted.
- A user is created or deleted.
- The sales representative for an account is changed to a different employee.
- The employee ID of a user is changed.
When one of these changes happens, it will:
- Identify the sales rep user for each affected account.
- If there is no existing UserAccount record for that sales rep, insert it.
- If there is an existing UserAccount record for a prior sales rep, delete it.
This to be accomplished via an impressive combination of replication, triggers, and cursors.
The problems There are a number of problems with this approach.
- It would take a DBA a couple of weeks to build and test it.
- Testing cannot be easily automated.
- It has too many moving parts (a.k.a. degrees of freedom), each one of which would have to be monitored.
- It is difficult to determine whether a UserAccount record was created automatically or intentionally. We could either delete UserAccount records that a human created, or fail to delete UserAccount records that a program created.
- It does not take effect immediately. People could be diagnosing a failure of the system, only to learn that the process hasn't yet run.
These are all symptomatic problems. Each one could be addressed individually. But here are the two big systemic problems:
- The use case dictated a solution.
- The solution treated dependent behavior as independent behavior.
Here's my solution Use case 1019 has been deleted, and replaced with a single business rule in use case 1020: User Account Selection. This rule reads "A user may select any account for which their employee ID matches the accounts sales representative employee ID." No mention of how this is accomplished. That solves one problem.
To solve the other, we simply change the query that fetches the list of accounts at the beginning of the shopping experience. Instead of just getting UserAccount records, we also include accounts where account.SalesRepEmployeeID = user.EmployeeID. One small change to a query, instead of a rats nest of triggers.
The basic flaw in the Rube Goldberg reasoning that produced the original design was that it treated dependent behavior (which accounts the user could select) as independent behavior (UserAccount records). Changes to independent behavior (INSERTs, UPDATEs, and DELETEs) are permanent. Changes to dependent behavior (SELECTs) are not. It is therefore harder to code, test, monitor, and troubleshoot independent behavior. It is easier to do all of those things with dependent behavior.
Records are independent. Queries are dependent. Never use independent behavior when dependent behavior is sufficient.
Posted in Databases, dof | No Comments »
May 24th, 2009
For the Commuter application, I want to rate podcasts. But instead of giving them a star rating, I decided to give them a "car" rating.
I drive a blue Beetle, so naturally I decided that that would make the perfect template for my rating buttons. Here's how to draw a Beetle in Expression Blend.
Start with two circles for tires, and two circles for wheel wells. Choose the ellipse tool, and hold Shift while you drag from one corner to the opposite. Draw a small circle inside a big circle, then select both, hold Ctrl, and drag to create copies.
Next, draw an ellipse for the body. Don't hold Shift while you drag, so the shape is unconstrained. The ellipse goes through the center of the front wheel, and is tangent with the back wheel. Switch to the rectangle tool to add a rounded rectangle for the bottom of the car.
Now we're going to use "Combine" operations to trim the shapes. When you combine two shapes, the originals are lost. We want to combine the rounded rectangle with several shapes, so we need to make copies of it.
The first operation will be to cut one wheel well with the rounded rectangle. Make a copy of the rectangle, select both it and one wheel well, and select "Combine: Intersect" from the "Object" menu. Repeat for the other wheel well, and for the body.

We are done with the rounded rectangle, so we can delete the original. Next, select the two wheel wells and the body and "Combine: Unite" them. There will be a strange dip where the front wheel well meets the windshield. We'll straighten this out by adding a rectangle.
Unite the rectangle with the outline of the car to complete the picture.
Now we can use this path as the basis for rating buttons.
Posted in Commuter | No Comments »
|