WPF

WPF: Animate Your Button’s Enabled/Disabled Event

Posted on Updated on

Probably this sounds really easy. I’ll bet you’re thinking “all you have to do is create a style with an event trigger for the event ‘IsEnabledChanged‘ and plug-in some animation.” Unfortunately, this won’t fly because event triggers only work for routed events, and IsEnabledChanged is not a routed event.

Animation Reduces Confusion for State Changes

Why would you want to animate a button? To call attention to it, so the user realizes they now have the power to do something they didn’t before. Little touches like this can greatly reduce training time for your apps, and generate good will with your users. According to this interview on DotNetRocks, you can reduce the overall cost of your app by 10% – 40% by making it more intuitive this way. (Note that you need to do more than animate just one button!) Back to the correct animation approach. You can’t do it the obvious way, using an event trigger. That leaves you with three approaches:

  1. Write a data trigger
  2. Create your own routed event and raise it in code when the button’s IsEnabledChanged event fires
  3. Create your own ControlTemplate and utilize the VisualStateManager to provide animations

Options one and two have this disadvantage: the animation is not automatic. You have to set some flag in your code to signal to the data trigger that you want the animations to fire, or else raise your custom event. Effectively you would perform two operations in code every time you disable your button (set enabled state, then set the code flag). I like option three, even though it requires more work to set up. After setting up, it is easier to use because the animation happens automatically. Benefit: for big projects, you write the template once and never have to look at  again. You also gain the option to customize your button in other ways, which I won’t discuss here. To use VisualStateManager, you create a style with a ControlTemplate, my research has not uncovered any other way. That requires you to custom-build most of your button’s display features. Here is a screen shot of the enabled and disabled states of my sample:

Screen shot showing Enabled and Disabledabled States
Screen shot showing the enabled and disabled states. Color differences are slightly exaggerated here to highlight the different states.

To mimic the normal button appearance, I used two rectangles, one on above the other, generating the button’s appearance. Examine the left above: the dark gray top of the button, comes from one of my rectangles. The black bottom part comes from my other rectangle. The disabled appearance on the right uses the same two rectangles, but the animation changes the colors to White and WhiteSmoke. I supply a ColorAnimation for each rectangle.

You can’t tell from the screen shot, but I elected to send the colors through 3 states to make it obvious something has happened to the button:

  1. From disabled colors
  2. Through pale yellow colors
  3. To enabled colors

Or vice-versa when changing the other way, normal to disabled.

Here is the style that uses the VisualStateManager:

<Style TargetType="Button" x:Key="AnimatedEnableButtonStyle" >
    <Setter Property="Margin" Value="5" />

    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="Button">
                <Border BorderBrush="Black" BorderThickness="1" CornerRadius="2" 
                           TextBlock.Foreground="Black" 
                           RenderTransformOrigin=".5,.5" Name="theBorder" >
                    <Border.RenderTransform>
                        <TransformGroup>
                            <ScaleTransform x:Name="scaleTransform" />
                        </TransformGroup>
                    </Border.RenderTransform>
                    <Grid >
                        <Grid.RowDefinitions>
                            <RowDefinition />
                            <RowDefinition />
                        </Grid.RowDefinitions>
                        <Rectangle Name="topBackground" Fill="WhiteSmoke"  />
                        <Rectangle Grid.Row="1" Name="bottomBackground" 
                                   Fill="LightGray" />
                        <ContentPresenter Grid.RowSpan="2" 
                               VerticalAlignment="Center" 
                               HorizontalAlignment="Center" Margin="5 0" />
                    </Grid>
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup Name="CommonStates">
                            <VisualState Name="Disabled">
                                <Storyboard>
                                    <ColorAnimation Storyboard.TargetName="topBackground"
                                          Storyboard.TargetProperty="(Rectangle.Fill).(Color)"
                                          To="White" Duration="0:0:.5" />
                                    <ColorAnimation Storyboard.TargetName="bottomBackground"
                                          Storyboard.TargetProperty="(Rectangle.Fill).(Color)"
                                          To="WhiteSmoke" Duration="0:0:0.5" />
                                    <ColorAnimation Storyboard.TargetName="theBorder"
                                          Storyboard.TargetProperty="(TextBlock.Foreground).(Color)"
                                          To="Gray" Duration="0:0:0.5" />
                                </Storyboard>
                            </VisualState>

                            <VisualState Name="Normal">
                                <Storyboard>
                                    <ColorAnimation Storyboard.TargetName="topBackground"
                                          Storyboard.TargetProperty="(Rectangle.Fill).Color"
                                          To="PeachPuff" Duration="0:0:0.5" />
                                    <ColorAnimation Storyboard.TargetName="topBackground"
                                          Storyboard.TargetProperty="(Rectangle.Fill).Color"
                                          BeginTime="0:0:0.5"
                                          To="WhiteSmoke" Duration="0:0:0.5" />
                                        
                                    <ColorAnimation Storyboard.TargetName="bottomBackground"
                                          Storyboard.TargetProperty="(Rectangle.Fill).Color"
                                          To="LightYellow" Duration="0:0:0.5" />
                                    <ColorAnimation Storyboard.TargetName="bottomBackground"
                                          BeginTime="0:0:0.5"
                                          Storyboard.TargetProperty="(Rectangle.Fill).(Color)"
                                          To="LightGray" Duration="0:0:0.5" />
                                        
                                    <ColorAnimation Storyboard.TargetName="theBorder"
                                          Storyboard.TargetProperty="(TextBlock.Foreground).Color"
                                          To="Black" Duration="0:0:0.5" />

                                    <ColorAnimation Storyboard.TargetName="theBorder"
                                          Storyboard.TargetProperty="BorderBrush.Color"
                                          From="Black" To="DarkBlue" 
                                          Duration="0:0:1" AutoReverse="True" />
                                </Storyboard>
                            </VisualState>
                            <VisualState Name="Pressed">
                                <Storyboard>
                                    <DoubleAnimation Storyboard.TargetName="scaleTransform"
                                          Storyboard.TargetProperty="ScaleX" 
                                          To=".9" Duration="0" />
                                    <DoubleAnimation Storyboard.TargetName="scaleTransform"
                                          Storyboard.TargetProperty="ScaleY"
                                          To=".9" Duration="0" />
                                </Storyboard>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Using the control style is pretty simple, but,  just to make sure, here is a sample showing how you will use it:

<Button Content="Test Button" Name="btnTest" 
            IsEnabled="{Binding ElementName=chkEnabled,Path=IsChecked}" 
            Height="30" Margin="5" 
            Click="btnTest_Click"
            Style="{StaticResource AnimatedStyle}" />

Dissecting the Style

Note that my control template completely replaces the normal button appearance. Should you desire, you can capitalize on this to make custom buttons, such as round buttons. Note that I define my button using

  1. A border, with a name
  2. An empty scale transform, with a name,
  3. A grid
  4. Containing two rectangles, each named.
  5. A content presenter

Tip: provide names (such as for my named border, rectangle and scale transform) to make it easier to specify the target of your animations. As discussed above, my button appearance mimics the standard button, other than the animations. This requires two rectangles, the top and bottom of the button.

My Control States

Note that I have provided animation for three different control states:

  1. Disabled
  2. Normal
  3. Pressed

I’ve looked around for a complete list of states, all I could find was Karen Corby’s Blog post. For normal and disabled states, I provide an ColorAnimation, cycling from dull colors, through pale yellows, to the normal enabled colors. Not that ColorAnimation allows you to specify a BeginTime; that is how you make your colors change through more than one state. For example, you start at the disabled color, use a ColorAnimation to move to a pale yellow, then, start your next animation when the first one ends, using the BeginTime; the effect is one animation starting after the other ends. For the pressed state, I use a scale transform to briefly shrink the button by 10 % (to .9).

Summary

Animations make it apparent to the user that something has changed and reduce training costs, making your app more profitable overall. You use a VisualStateManager inside a ControlTemplate to provide animations for each state (disabled, pressed, normal).

Use DataTriggers to Initiate WPF Animation From Code

Posted on Updated on

WPF has some great animation abilities; you might think they are toys, but in reality they are important, under-utilized tools for making your app easier to use. Use animation to draw attention to things that change or which users should pay attention to. Some studies have suggested that this kind of UI improvement can reduce training costs and improve your users’ efficiency by 10-40%.

You can initiate animation several ways, including when some routed event occurs. However, some not everyone knows how to start animation from code. A great way to do it is to use DataTriggers.

Screen shot showing animated label
A timer (in code) counts-down to zero, then makes the label flash red/white.

My little sample uses a timer (in code) to initiate flashing, but you could use any other code you like, so long as it sets the property “StartFlashing” to true. But we’ll get to that shortly. First, let’s start be defining a style that uses animation.

<Window.Resources>
  <Style x:Key="timerTriggeredFlash" TargetType="Label">
    <Style.Triggers>
      <DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=Window},
         Path=DataContext.StartFlashing}" Value="True">
          <DataTrigger.EnterActions>
             <BeginStoryboard>
               <Storyboard>
                  <ColorAnimation 
                     Storyboard.TargetProperty="(Background).(SolidColorBrush.Color)" 
                     From="White" To="Red" RepeatBehavior="3x" 
                     AutoReverse="True" />
                   </Storyboard>
             </BeginStoryboard>
          </DataTrigger.EnterActions>
      </DataTrigger>
    </Style.Triggers>
  </Style>
</Window.Resources>

Note that

  1. You will reference the style below using its name, “timerTriggerdFlash”
  2. Your trigger references a property in your code named “StartFlashing”, it should be boolean
  3. When you set that property to true, the animation starts
  4. The target property is a bit tricky; basically the syntax references an attached property of the background’s brush

The next thing to do is reference the style in your label, as shown here:

<Label Grid.Row="1" Style="{StaticResource timerTriggeredFlash}" Content="Flashing label" />

Pretty simple, but it doesn’t hurt to spell things out!

Now, for the property that initiates the flashing (StartFlashing). We will bind our form to a ViewModel (class) and expose that property publicly, making sure to implement INotifyPropertyChanged. (Alternatively, you could make your property a DependcencyProperty, which I prefer, but which requires additional explanation.)

I think it is easiest if I show the entire ViewModel class:

public class MainWindowViewModel : INotifyPropertyChanged {
	DispatcherTimer _Timer;

	//Constructor
	public MainWindowViewModel() {
		_Timer = new DispatcherTimer();
		_Timer.Interval = TimeSpan.FromSeconds(1);
		_Timer.Tick += new EventHandler(_Timer_Tick);
		_SecondsLeft = 3;
		_Timer.Start();
	}

	//This event is fired every second, when the timer goes off
	void _Timer_Tick(object sender, EventArgs e) {
		SecondsLeft = SecondsLeft - 1;
		if (_SecondsLeft <= 0) {
			StartFlashing = true;
			_Timer.Stop();
		}
	}

	//This property controls our DataTrigger!
	private bool _StartFlashing;
	public bool StartFlashing {
		get { return _StartFlashing; }
		set { 
			_StartFlashing = value;
			if (PropertyChanged != null) {
				PropertyChanged(this, new PropertyChangedEventArgs("StartFlashing"));
			}
		}
	}
	
	//This property is displayed as the countdown seconds:
	private int _SecondsLeft;
	public int SecondsLeft {
		get { return _SecondsLeft; }
		set { 
			_SecondsLeft = value;
			if (PropertyChanged != null) {
				PropertyChanged(this, new PropertyChangedEventArgs("SecondsLeft"));
			}
		}
	}

	public event PropertyChangedEventHandler PropertyChanged;
}

Note that

  1. In the constructor, we create a timer that fires every second and initialize our countdown to 3 seconds
  2. When the timer goes-off, it decrements the countdown
  3. When the countdown hits zero, it stops the timer and sets our property “StartFlashing” to True
  4. Which triggers the animation

In case you haven’t done this before, you need to tell your form where to get its binding from, by assigning a DataContext. Our DataContext will be our ViewModel, and we assign it in our form’s constructor, like this:

public partial class MainWindow : Window {
	public MainWindow() {
		InitializeComponent();
		this.DataContext = new MainWindowViewModel();
	}
}

The last thing is to display the form’s XAML.

<Window x:Class="AnimatedAlarm.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Start Animation From Code With Data Binding" Height="200" Width="300"
        WindowStartupLocation="CenterScreen" >
    
    <Window.Resources>
        <Style x:Key="timerTriggeredFlash" TargetType="Label">
            <Style.Triggers>
                <DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=Window},
                            Path=DataContext.StartFlashing}" Value="True">
                    <DataTrigger.EnterActions>
                        <BeginStoryboard>
                            <Storyboard>
                                <ColorAnimation 
                                    Storyboard.TargetProperty="(Background).(SolidColorBrush.Color)" 
                                    From="White" To="Red" RepeatBehavior="3x" 
                                    AutoReverse="True" />
                            </Storyboard>
                        </BeginStoryboard>
                    </DataTrigger.EnterActions>
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </Window.Resources>
    
    <Grid>        
        <Label Content="Starts Flashing in :" />
        <TextBlock Grid.Column="1" Text="{Binding SecondsLeft}" 
                   VerticalAlignment="Center" />

    <Label Grid.Row="1" 
           Style="{StaticResource timerTriggeredFlash}" 
           Content="Flashing label" />
 
        <Grid.RowDefinitions>
            <RowDefinition Height="auto" />
            <RowDefinition Height="auto" />
            <RowDefinition />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="auto" />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
   </Grid>
</Window>

In summary, use a DataTrigger when you need to initiate animation from code. Declare a style for your control and, in the data trigger, bind to a property from your form’s DataContext, typically a ViewModel. In your code, make sure that your bound property (e.g. StartFlashing) raises the PropertyChanged event, or else is a DependencyProperty.

Download the code here!

Puzzle Victory – Word Synonym Pair/Remove Double-S

Posted on Updated on

The challenge:

What word, containing two consecutive S’s, becomes its own synonym if you drop those S’s?

Link to the challenge: http://www.npr.org/2014/01/26/266210037/take-synonyms-for-a-spin-or-pirouette

Screen Shot Showing Solution to Puzzle
Screen Shot Showing Solution to Puzzle

Fun With Linq!

Since I had a nice synonym list already, I was able to dive into this one and come up with an elegant solution using a couple lines of Linq (Language INtegrated Query). “Linq” is Microsoft’s built-in query language that allows you process all kinds of lists and a lot more.

Sorry, faithful readers, I don’t remember exactly where I got my synonym list, but you can get a similar list  here, even it is formatted differently than mine.

Algorithm

The following code snippet analyzes a single line from my file and puts the result into the variable ‘answer’ if a solution is found. It does almost all the work, yet takes only six lines. A detailed explanation follows, but if you examine it closely, you should be able to figure-out most of it:

var answer = from word1 in aLine.Split('|')
                let wp = new { Original = word1, WordNoSS = word1.Replace("ss", "") }
                join word2 in aLine.Split('|') on wp.WordNoSS equals word2
                where word1.Contains("ss") 
                    && !distinctAnswers.Contains(word1)
                select wp;

Detailed Explanation

  1. Open the synonym file fore reading, using a stream reader named ‘sr’
  2. Each line contains synonyms, separated by a pipe character, like this:
    • bloom|blossom|flower|flourish|coloration
  3. Within a  Linq query, split every line  from the file on the | character,
  4. Into an array,
  5. Name  each array entry ‘word1’
  6. Use the Linq ‘where’ operator to select only entries in the array containing ‘ss’
  7. Join the first Linq query against another query
  8. Which we build by again splitting the line, referring to its entries as ‘word2’
  9. If the join operation has any match whatsoever,
  10. Then we have a potential solution
  11. But reject any answer we have already discovered, by comparing current against previous entries from a list called ‘distinctAnswers’
  12. If our potential answer is not in the list, display the answer, and add it to the distinctAnswers list

Here’s the entire code listing

using System;
using System.Collections.Generic;   //For my list (of word pairs)
using System.Windows;
using System.IO;                    //To open synonyms file
using System.Windows.Input;         //To display hourglass while waiting
using System.Linq;

namespace Jan26_2014 {
    /// <summary>Code to solve the puzzleThe buttonEvent args();
    /// <summary>Code to solve the puzzle</summary>
    /// <remarks>
    /// Solution loads a list of synonyms from a file
    ///     Each line has several synonyms on it, separated by |
    /// Use split the line into an array of words and use linq on it.
    /// Linq provides an elegant solution that is also pretty efficient.
    /// </remarks>
    public partial class MainWindow : Window {

        private const string SYNONYM_FILE = 
                @"C:\Users\Randy\Documents\Puzzles\Data\Synonyms.txt";

        /// <summary>Default constructor</summary>
        public MainWindow() {
            InitializeComponent();
        }

        /// <summary>
        /// Fires when user clicks the Solve button
        /// </summary>
        /// <param name="sender">The button</param>
        /// <param name="e">Event args</param>
        private void btnSolve_Click(object sender, RoutedEventArgs e) {
            Mouse.OverrideCursor = Cursors.Wait;
            txtAnswer.Clear();

            //Open the file for reading
            using (StreamReader sr = File.OpenText(SYNONYM_FILE)) {
                //Each line in the file contains words separted by |
                //There is an issue with the file (for our purposes)
                //For example, both these lines are in the file:
                //  - bloom|blossom|flower|flourish|coloration
                //  - blossom|bloom|flower|flourish

                //Use the list of distinct answers to deal with that issue and 
                //avoid reporting the same answer twice:
                List<string> distinctAnswers = new List<string>();
                //Keep reading while there is more to be read
                while (sr.Peek() != -1) {
                    string aLine = sr.ReadLine();

                    //Use Linq to join words lacking 'ss' against the same words in the the line 
                    var answer = from word1 in aLine.Split('|')
                           //Hold word1 in anonymous class/1)original value 2)word lacking 'ss'
                           let wp = new { Original = word1, WordNoSS = word1.Replace("ss", "") }
                           //Word2 values also come from the words in the line
                           join word2 in aLine.Split('|') on wp.WordNoSS equals word2
                           //Only use word1 if it has ss and we haven't found it already
                           where word1.Contains("ss") 
                                 && !distinctAnswers.Contains(word1)
                           select wp;

                    //'answer' is an IEnumerable (a kind of list) containing entries
                    //of an anonymous type; each entry has properties 'Original' and 'WordNoSS'
                    //If the list has any entry, each will be a valid answer pair
                    foreach (var wp in answer) {
                            txtAnswer.Text += wp.Original + " - " + wp.WordNoSS + "\n";
                            distinctAnswers.Add(wp.Original);
                    }
                }
            }
            MessageBox.Show("Done Solving", "Mission Accomplished",
                            MessageBoxButton.OK, MessageBoxImage.Information);
            Mouse.OverrideCursor = null;
        }
    }
}

Now, here’s the XAML, you should be able to run it except for the background, which you can either comment-out, or else download yourself from here.

A couple comments:

  • Since the solution was related to flowers, I decided to use a field of flowers for the background
  • And make the answer textbox partly transparent using an opacity setting of .5
  • The title bar uses a drop shadow effect for a little pizzaz
  • Like previous puzzle solutions, I elected to draw my own icon instead of linking to a file
<Window x:Class="Jan26_2014.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Synonym Pair with Double-S" Height="350" Width="710"
        WindowStartupLocation="CenterScreen" >

    <Grid>
        <!-- The Title with a drop shadow effect -->
        <TextBlock FontSize="36" Grid.ColumnSpan="3"
                Text="Two Synonyms Spelled Same Except for SS" 
                Foreground="DarkRed" HorizontalAlignment="Center">
            <TextBlock.Effect>
                <DropShadowEffect BlurRadius="5" Color="LightGray" 
                                    Opacity=".8" />
            </TextBlock.Effect>
        </TextBlock>

        <!-- The Challenge textblock -->
        <TextBlock Grid.Row="1" Grid.ColumnSpan="5" TextWrapping="Wrap" 
                   FontSize="16" Margin="3">
            <Bold>Challenge:</Bold> What word, containing two consecutive S's, 
            becomes its own synonym if you drop those S's?
        </TextBlock>

        <!-- The 3rd row has a label/textbox for the answer, plus the Solve button-->
        <Label Grid.Row="2" Content="Answer(s):" 
               Target="{Binding ElementName=txtAnswer}" 
               VerticalAlignment="Top" FontSize="16" FontWeight="Bold" />
        <TextBox Grid.Row="2" Grid.Column="1" Name="txtAnswer" 
                 TextWrapping="Wrap" AcceptsReturn="True" Opacity=".5" 
                 FontWeight="Black" FontSize="16"
                 />
        <Button Grid.Row="2" Grid.Column="2" Name="btnSolve" 
                Content="_Solve" Height="30" 
                FontSize="16" FontWeight="Black"
                VerticalAlignment="Top" Margin="3" Click="btnSolve_Click" />

        <!-- Comment-out the next 3 lines if you don't have a background file with that name -->
        <Grid.Background>
            <ImageBrush ImageSource="/Jan26_2014;component/Images/Flowers.jpg" Opacity=".6" />
        </Grid.Background>

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="auto" />
            <ColumnDefinition />
            <ColumnDefinition Width="auto" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto" />
            <RowDefinition Height="auto" />
            <RowDefinition />
        </Grid.RowDefinitions>
    </Grid>

    <!-- I elected to use a icon drawn in XAML, a flower was too hard -->
    <Window.Icon>
        <DrawingImage>
            <DrawingImage.Drawing>
                <GeometryDrawing Brush="Gold">
                    <GeometryDrawing.Pen>
                        <Pen Brush="Yellow" Thickness="1" />
                    </GeometryDrawing.Pen>
                    <GeometryDrawing.Geometry>
                        <EllipseGeometry RadiusX="25" RadiusY="25" />
                    </GeometryDrawing.Geometry>
                </GeometryDrawing>
            </DrawingImage.Drawing>
        </DrawingImage>
    </Window.Icon>
</Window>

That’s all, good luck!

NPR Puzzle: Famous Person with 4 Double-Letters in Name

Posted on

The Challenge:

Name a famous person whose first and last names together contain four doubled letters — all four of these being different letters of the alphabet. Who is it? For example, Buddy Holly’s name has two doubled letters, D and L.

Link to challenge

Screen shot showing solution
Screen shot showing some of the over 40 matching names

Well, that was kind of fun, and I enjoyed the results. For starts, I found over 40 answers. For seconds, I love some of the names that qualify, like ‘Bobby “Boogaloo” Watts‘ (2 b’s, 2 o’s, 2 o’s again, 2 t’s), ‘Dimebag Darrell Abbott‘, ‘Boccaccio Boccaccino‘, ‘Blind Mississippi Morris’ or ‘William M. “Boss” Tweed‘ (qualifies because the trailing ‘m’ in William pairs with his middle initial. While they all qualify, I’m sure the person Will was thinking of was Tennessee Williams.

Algorithm

As you might guess, solving the puzzle is easy if you have the right list. But getting that list was quite a challenge! First, let’s discuss processing the list(s) of names to solve.

As you can see from my screen shot, I elected to make a number of name lists, motivated by the mechanics of capturing them from Wikipedia. In terms of solving the puzzle, that just means we need a loop to process each file. We read each file one name at a time. When we read a name, we use another loop to process the letters in that name. By ‘processing’, I mean that we examine the letter and compare it to the previous letter, if they match, we increase our counter. After processing the line, if our counter equals 4, we have a winner!

List answerList = new List();
string folder = Path
                .GetDirectoryName(Assembly
                .GetExecutingAssembly()
                .Location);

//Get all the text files in the exe folder, hold result in an array:
string[] fileList = Directory.GetFiles(folder, "*.txt");
//process every file we found
foreach(string aFile in fileList) {
    using (StreamReader sr = File.OpenText(aFile)) {
        while (sr.Peek() != -1) {
            string aPerson = sr.ReadLine();
            //Remove blanks convert to lower case
            string letters = aPerson.ToLower().Replace(" ", "");
            //reject some bad names that contain bad letters like ( or ,
            if (!letters.Contains("(") && !letters.Contains(",")) {
                char lastLtr = letters[0];
                int hitCount = 0;
                //loop through the letters
                for (int i = 1; i < letters.Length; i++) {
                     if (letters[i] == lastLtr) {
                         hitCount++;
                         //avoid triple letters by resetting previous
                         lastLtr = '';
                     } else {
                         lastLtr = letters[i];
                     }
                 }
                 if (hitCount >= 4) {
                    //look for previous entries:
                    NameFilePair prev = answerList.FirstOrDefault(n => n.Person == aPerson);
                    if (prev == null) {
                        NameFilePair nfp = new NameFilePair {
                            FileName = Path.GetFileName(aFile),
                            Person = aPerson
                        };
                        answerList.Add(nfp);
                    }
                }
            }
        }
    }
}

Downloading the Name Lists

As I mentioned above, the hardest part was getting the lists of names. As usual, Wikipedia had the most complete list, but scattered across many pages. I was able to start at this page http://en.wikipedia.org/wiki/Lists_of_people_by_nationality and programmatically follow the links on every page to other pages. Basically, my code loads the main page, identifies two kinds of links on the page:

  • Links to other pages of name lists
  • Links to actual articles about people

On most pages, the same pattern is used for each kind of link, though I had to do a bit of manual cleaning. As usual, I utilized regular expressions to match the text.

Here’s the complete code listing:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.IO;                        //To open/close files
using System.Net;                       //For WebClient
using System.Text.RegularExpressions;   //To match text patterns
using System.Windows.Input;             //For Mouse override cusor
using System.ComponentModel;            //For Background worker
using System.Reflection;                //To get exe path

namespace Jan19_2014 {
    /// <summary>Downloads names and solves the puzzle</summary>
    public partial class MainWindow : Window {

        //Allows us to display a cancel button and update the status bar on different thread
        BackgroundWorker _WikipediaFetcher;

        /// <summary>Default constructor</summary>
        public MainWindow() {
            InitializeComponent();
        }

        /// <summary>
        /// Fires when user clicks the button 'Solve'
        /// </summary>
        /// <param name="sender">The button</param>
        /// <param name="e">Event args</param>
        /// <remarks>
        /// Iterates all the files in the project folder having 'txt' extension,
        /// approximately 705 total.
        /// 
        /// Examines every name in each file by
        ///     - Concatenating the letters
        ///     - Looping through the result
        ///     - Comparing the last letter to the current
        ///     - Counting as a hit when the prev letter matches current
        ///     - Adding to the answer list when the hit count equals 4
        /// </remarks>
        private void btnSolve_Click(object sender, RoutedEventArgs e) {
            List<NameFilePair> answerList = new List<NameFilePair>();
            string folder = Path
                            .GetDirectoryName(Assembly
                                            .GetExecutingAssembly()
                                            .Location);
            
            //Get all the text files in the exe folder:
            string[] fileList = Directory.GetFiles(folder, "*.txt");
            //process every file we found
            foreach(string aFile in fileList) {
                using (StreamReader sr = File.OpenText(aFile)) {
                    while (sr.Peek() != -1) {
                        string aPerson = sr.ReadLine();
                        //Remove blanks convert to lower case
                        string letters = aPerson.ToLower().Replace(" ", "");
                        //reject some bad names that contain bad letters
                        if (!letters.Contains("(") && !letters.Contains(",")) {
                            char lastLtr = letters[0];
                            int hitCount = 0;
                            //loop through the letters
                            for (int i = 1; i < letters.Length; i++) {
                                if (letters[i] == lastLtr) {
                                    hitCount++;
                                    //avoid triple letters by resetting previous
                                    lastLtr = '';
                                } else {
                                    lastLtr = letters[i];
                                }
                            }

                            if (hitCount >= 4) {
                                //look for previous entries:
                                NameFilePair prev = answerList.
                                            FirstOrDefault(n => n.Person == aPerson);
                                if (prev == null) {
                                    NameFilePair nfp = new NameFilePair {
                                        FileName = Path.GetFileName(aFile),
                                        Person = aPerson
                                    };
                                    answerList.Add(nfp);
                                }
                            }
                        }
                    }
                }

            }
            //bind the results to the grid
            grdAnswer.ItemsSource = answerList;

            MessageBox.Show("Solving Done", "Mission Accomplished", 
                MessageBoxButton.OK, MessageBoxImage.Information);
        }

        /// <summary>
        /// Fired when user clicks the button to get Wikipedia people
        /// </summary>
        /// <param name="sender">The button</param>
        /// <param name="e">Event args</param>
        /// <remarks>
        /// Sets-up the background worker and fires it off.
        /// </remarks>
        private void btnGetWikipediaRecursive_Click(object sender, RoutedEventArgs e) {
            if ((string)btnGetWikipediaRecursive.Content == "_Go") {
                _WikipediaFetcher = new BackgroundWorker();
                _WikipediaFetcher.WorkerSupportsCancellation = true;
                _WikipediaFetcher.WorkerReportsProgress = true;
                _WikipediaFetcher.RunWorkerCompleted += 
                    new RunWorkerCompletedEventHandler(WikipediaFetcher_RunWorkerCompleted);
                _WikipediaFetcher.ProgressChanged += 
                    new ProgressChangedEventHandler(WikipediaFetcher_ProgressChanged);
                _WikipediaFetcher.DoWork += new DoWorkEventHandler(WikipediaFetcher_DoWork);

                tbProgress.Visibility = System.Windows.Visibility.Visible;
                btnGetWikipediaRecursive.Content = "_Cancel";

                _WikipediaFetcher.RunWorkerAsync(txtWikipediaPeopleUrl.Text);
            } else {
                btnGetWikipediaRecursive.Content = "_Go";
                _WikipediaFetcher.CancelAsync();
            }
        }

        /// <summary>
        /// Fires when the background worker is kicked-off
        /// </summary>
        /// <param name="sender">The worker</param>
        /// <param name="e">Event args containing the URL as the e.Argument</param>
        /// <remarks>
        /// This method will start visiting a Wikipedia page that has links
        /// to other pages which may, in turn, contain other pages.
        /// 
        /// In general, each page has two types of payload we care about:
        ///     - the link to another page
        ///     - a person's name
        /// We identify each with a regular expression, but in this method,m
        /// we initialize the to regular expressiosn and merely pass them
        /// to a subroutine 'CapturePeople'.
        /// 
        /// This method iterates a number of links on the main page (which
        /// is rather irregular, so we set-up boundaries within which we search)
        /// and calls CapturePeople on each. CapturePeople is recursive,
        /// so it will handle any additional links therein.
        /// </remarks>
        void WikipediaFetcher_DoWork(object sender, DoWorkEventArgs e) {
            string peoplePattern = @"
                                    <li><a			#Literal match
                                    [^>]+?			#Anything except >
                                    >				#Literal match
                                    ([^<]+?)		#Capture group, anything except <
                                    <				#Literal match
                                    ";
            Regex rePeople = new Regex(peoplePattern, 
                RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline);

            List<string> visitedUrlList = new List<string>();
            using (WebClient wc = new WebClient()) {
                string wikiUrl = e.Argument as string;
                using (StreamReader sr = new StreamReader(wc.OpenRead(wikiUrl))) {
                    string allText = sr.ReadToEnd();
                    int p0 = allText.IndexOf("class=\"div-col columns column-width");
                    int p1 = allText.IndexOf("id=\"See_also");
                    string payLoad = allText.Substring(p0, p1 - p0);


                    string linkPattern = @"
                            <li><a		#Literal match
                            \s			#Whitespace
                            href=""		#Literal match
                            (/wiki/List_of_[^""]+?)	#Capture group, anything but quote
                            ""			#Literal match
                            ";
                    Regex reLink = new Regex(linkPattern, 
                        RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace);
                    Match m = reLink.Match(payLoad);
                    while (m.Success) {
                        _WikipediaFetcher.ReportProgress(0, m.Groups[1].Value);
                        if (_WikipediaFetcher.CancellationPending) {
                            break;
                        }
                        CapturePeople(m.Groups[1].Value, visitedUrlList, 
                                        wc, reLink, rePeople);

                        m = m.NextMatch();
                    }
                }
            }
        }

        /// <summary>
        /// Captures the people listed on the URL in question, writing to a file based on
        /// the URL, and follows any links on the page to additional lists.
        /// </summary>
        /// <param name="listUrl">The Url to load and capture people form</param>
        /// <param name="visitedUrlList">List of links we have already visited</param>
        /// <param name="wc">Web client to fetch web pages</param>
        /// <param name="reLink">Regex that identifies a typical Wikipedia list link</param>
        /// <param name="rePeople">Regex that identifies a typical Wikipedia person name</param>
        private void CapturePeople(string listUrl, List<string> visitedUrlList, 
                                WebClient wc, Regex reLink, Regex rePeople) {
            string url = "http://www.Wikipedia.org" + listUrl;
            int ndx = visitedUrlList.BinarySearch(url);
            if (ndx < 0)
                visitedUrlList.Insert(~ndx, url);
            else
                return;

            try {
                using (StreamReader sr = new StreamReader(wc.OpenRead(url))) {
                    string allText = sr.ReadToEnd();

                    //Limit matching to prior to these two IDs; generally
                    //any false matches that follow are for things like referecnes
                    int p = allText.IndexOf("id=\"See_also");
                    int p2 = allText.IndexOf("id=\"References");
                    Match linkMatch = reLink.Match(allText);
                    //iterate the matches
                    while (linkMatch.Success && linkMatch.Index < p) {
                        if (_WikipediaFetcher.CancellationPending)
                            return;

                        _WikipediaFetcher.ReportProgress(0, listUrl);
                        if (!url.EndsWith(linkMatch.Groups[1].Value)) {
                            CapturePeople(linkMatch.Groups[1].Value, 
                                    visitedUrlList, wc, reLink, rePeople);
                        }
                        linkMatch = linkMatch.NextMatch();
                    }

                    _WikipediaFetcher.ReportProgress(0, url);
                    Match m = rePeople.Match(allText);
                    int p3 = allText.IndexOf("class=\"mw-headline");
                    if (p3 >= 0)
                        allText = allText.Substring(p3);
                    string fName = listUrl.Substring(listUrl.LastIndexOf("/") + 1) + ".txt";
                    using (StreamWriter sw = File.CreateText(fName)) {
                        while (m.Success && (p == -1 || m.Index < p) &&
                                            (p2 == -1 || m.Index < p2)) {
                            string person = m.Groups[1].Value;
                            sw.WriteLine(person);

                            m = m.NextMatch();
                        }
                    }
                }
            } catch (Exception ex) {
                Console.WriteLine(ex.Message);
            }

        }

        /// <summary>
        /// Fired when the background worker has something to report,
        /// namely, the URL it is processing
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void WikipediaFetcher_ProgressChanged(object sender, ProgressChangedEventArgs e) {
            tbProgress.Text = (string)e.UserState;
        }

        /// <summary>
        /// Fired when the background worker is done
        /// </summary>
        /// <param name="sender">The background worker</param>
        /// <param name="e">Event args</param>
        void WikipediaFetcher_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) {
            tbProgress.Visibility = System.Windows.Visibility.Hidden;
            MessageBox.Show("Done Capturing People", "Mission Accomplished", 
                             MessageBoxButton.OK, MessageBoxImage.Information);
        }
    }
}

Here’s my little class, used to hold a person and the file they came from:

    public class NameFilePair {
        public string Person { get; set; }
        public string  FileName { get; set; }
    }

Finally, here’s the XAML

<Window x:Class="Jan19_2014.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Famous Person With 4 Double Letters"
        WindowStartupLocation="CenterScreen"
        Height="380" Width="925">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="auto" />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        
        <!-- Left side/vertical letters -->
        <Border Background="Brown">
            <TextBlock FontSize="48" FontFamily="Impact" 
                       Foreground="LightGreen" VerticalAlignment="Top" >
                <ItemsControl ItemsSource="PUZZLE" />
            </TextBlock>
        </Border>
        
        <DockPanel Grid.Column="2">
            <!-- Status bar holds slider and progress display -->
            <StatusBar DockPanel.Dock="Bottom">
                <Label Content="Zoom:" Target="{Binding ElementName=sldZoom}" />
                <Slider Name="sldZoom" Width="50" Minimum=".5" Maximum="3" Value="1" />
                <TextBlock Name="tbProgress" Visibility="Hidden" />
            </StatusBar>
            
            <!-- Nested grid hods everything except the status bar and vertical PUZZLE -->
            <Grid >
                <Grid.LayoutTransform>
                    <!-- Works with the slider to enlarge/shrink the page -->
                    <ScaleTransform   ScaleX="{Binding ElementName=sldZoom,Path=Value}" 
                                      ScaleY="{Binding ElementName=sldZoom,Path=Value}" />
                </Grid.LayoutTransform>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="auto" />
                    <ColumnDefinition />
                    <ColumnDefinition Width="auto" />
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition Height="auto" />
                    <RowDefinition Height="auto" />
                    <RowDefinition Height="auto" />
                    <RowDefinition />
                </Grid.RowDefinitions>
                
                <Border Grid.ColumnSpan="3">
                    <Border.Background>
                        <LinearGradientBrush EndPoint="1,0.5" StartPoint="0,0.5">
                            <GradientStop Color="BurlyWood" Offset="1" />
                            <GradientStop Color="Brown" Offset="0" />
                        </LinearGradientBrush>
                    </Border.Background>
                    <TextBlock Text="Famous Person's Name with 4 Double Letters" 
                               FontFamily="Arial" FontStyle="Italic" 
                               FontSize="40" TextWrapping="Wrap" 
                               Foreground="LightGreen" VerticalAlignment="Center" >
                    </TextBlock>
                </Border>

                <TextBlock Grid.Row="1" Grid.ColumnSpan="3" TextWrapping="Wrap" 
                           FontSize="14" Margin="5 0">
                    <Bold>Next week's challenge from Ed Pegg Jr. of MathPuzzle.com:</Bold>
                    Name a famous person whose first and last names together 
                    contain four doubled letters — all four 
                    of these being different letters of the alphabet. Who 
                    is it? For example, Buddy Holly's name has two doubled 
                    letters, D and L.
                </TextBlock>
                
                
                <Label Grid.Row="2" Content="_Wikipedia Start URL:" 
                       Target="{Binding ElementName=txtWikipediaPeopleUrl}" 
                       VerticalAlignment="Center" />
                <TextBox Grid.Row="2" Grid.Column="1" Name="txtWikipediaPeopleUrl" 
                         Text="http://en.wikipedia.org/wiki/Lists_of_people_by_nationality"
                         VerticalAlignment="Center" />
                <Button Grid.Row="2" Grid.Column="2" Content="_Go" 
                        Name="btnGetWikipediaRecursive"
                        Height="30" Margin="3" Click="btnGetWikipediaRecursive_Click" />


                <Label Grid.Row="3" Content="_Answers:" 
                       Target="{Binding ElementName=txtAnswer}" />
                <DataGrid Grid.Row="3" Grid.Column="1" Name="grdAnswer" 
                          AutoGenerateColumns="True" />
                <Button Grid.Row="3" Grid.Column="2" Name="btnSolve" 
                        Content="_Solve" Height="30" Margin="5" 
                        Click="btnSolve_Click" Grid.ColumnSpan="2"
                        
                        VerticalAlignment="Top" />
            </Grid>
        </DockPanel>            
    </Grid>

    <!-- Build our own Icon using letters '4-2', meaing 4 doubles -->
    <Window.Icon>
        <DrawingImage>
            <DrawingImage.Drawing>
                <GeometryDrawing>
                    <GeometryDrawing.Geometry>
                        <RectangleGeometry Rect="0,0,20,20" />
                    </GeometryDrawing.Geometry>
                    <GeometryDrawing.Brush>
                        <VisualBrush>
                            <VisualBrush.Visual>
                                <TextBlock Text="4-2" FontSize="8"
                                                Background="Brown"
                                                Foreground="LightGreen" />
                            </VisualBrush.Visual>
                        </VisualBrush>
                    </GeometryDrawing.Brush>
                </GeometryDrawing>
            </DrawingImage.Drawing>
        </DrawingImage>
    </Window.Icon>
</Window>

Make the WPF GridSplitter Work Correctly!

Posted on Updated on

You can find lots of articles explaining how to use a WPF GridSplitter, but the ones I’ve seen never work quite right. Actually, they seem to work until you add some more realistic code, and then things start going haywire! In this post I explain a couple of gotcha’s, plus one nifty trick to give your splitter a 3-d appearance.

I dragged the splitter to the right, squashing the right-hand side
Now I have dragged the splitter to the right, squashing the right-hand side
GridSplitter sample the left-hand content is squashed
GridSplitter sample: the left-hand content is squashed when I drag my splitter left

 

 

The First Gotcha – The Wrong Column Stretches!

If you are trying your first GridSplitter, chances are you will omit a tag that looks completely unnecessary: the HorizontalAlignment tag (or, if you are splitting rows, the VerticalAlignment tag). If you omit that tag, your screen will look like this when you drag the splitter:

This is how it looks when you omit the HorizontalAlignment tag
This is how it looks when you omit the HorizontalAlignment tag

The correct XAML  looks like this:


<GridSplitter Grid.Column="1" ResizeDirection="Columns" Width="10" ToolTip="Drag left-right to resize" 
HorizontalAlignment="Stretch" />

Why do you need that tag? I wish I knew, I’ve never seen it discussed.

Gotcha Number Two

The next problem you’ll need to fight is when the wrong columns (or rows) get resized, or when nothing is resized whatsoever! Generally this happens when

  • You have 3 columns…
  • Your middle column holds the splitter…
  • And you auto-size either column zero, column 2, or both!

Important principle:If you specify the size of any row or column, even autosizing it, the GridSplitter cannot change it’s size!

For column zero, I specified 'width="auto". You can see that it's size is not affected by my GridSplitter, whereas column 2, with no width setting, responds correctly
For column zero, I specified ‘width=”auto”. You can see that its size is not affected by my GridSplitter, whereas column 2, with no width setting at all, responds correctly.

Further, if you have more than three columns, you should make sure that exactly two of them lack any width specification; they will be resized. If you have 4, 5, or more columns, please specify a width, typically ‘width=Auto”‘.

Tip: Give Your GridSplitter a 3-d Appearance

Even with a tool tip, your users might not realize that they should drag your GridSplitter around. (But they might get frustrated try knocking it around!) But, make it look 3-d and they will cotton-on pretty quick. The best way to do that is to

  • Use an ImageBrush for your background
  • If you’re using an older version of WPF, specify TileMode=”Tile” for the brush
  • I like to save a bit on resources and use the smallest possible image for my background (such as 8×1 pixels), letting the tile mode spread it across the available area.

Here is the XAML:

<GridSplitter Grid.Column="1" ResizeDirection="Columns" Width="10" ToolTip="Drag left-right to resize" 
               HorizontalAlignment="Center">
     <GridSplitter.Background>
         <ImageBrush ImageSource="/SplitterSample;component/Images/Splitter8x1.jpg" />
     </GridSplitter.Background>
</GridSplitter>

Here is what my background image looks like (magnified 8x):

This image is only 1 pixel high; WPF stretches it to tile the whole range necessary
This image is only 1 pixel high; WPF stretches it to tile the whole range necessary.

WPF will stretch (tile) your image to fill the available space, causing your image to magically appear like a 3-d bar!

Code Listing

Finally, here is the full XAML. If you wish to try it, make sure you create a sub-folder in your project named ‘Images’ and place your background image in that folder. Also, replace ‘SplitterSample’ with your project name where the ImageBrush is referenced.

<Window x:Class="SplitterSample.MainWindow"
 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 WindowStartupLocation="CenterScreen"
 Title="Grid Splitter Sample" Height="150" Width="325">
 <Grid>
     <Grid.ColumnDefinitions>
         <ColumnDefinition />
         <ColumnDefinition Width="auto" />
         <ColumnDefinition />
     </Grid.ColumnDefinitions>

     <TextBlock Background="LightGoldenrodYellow" Text="This is the left-hand side" />
     <GridSplitter Grid.Column="1" ResizeDirection="Columns" Width="10" ToolTip="Drag left-right to resize" 
                 HorizontalAlignment="Center" > 
          <GridSplitter.Background>
              <ImageBrush ImageSource="/SplitterSample;component/Images/Splitter8x1.jpg" />
          </GridSplitter.Background>
     </GridSplitter>

     <TextBlock Grid.Column="2" Background="Linen" Text="This is the right-hand side" />
 </Grid>
</Window>