C# Code, Tutorials and Full Visual Studio Projects

WordMerge – Word Fusion Application

Posted by on Oct 13, 2011 in Code Snippets, Projects, WPF | 0 comments

WordMerge – Word Fusion Application

WordMerge is a C# WPF project that is designed to show how you can modify a ListBox to change the selection behavior. Take a look at the video and you’ll get a quick idea of how it works.

Download the Source Code

 

 

How it works

The window is simple with two ListBoxes displaying the words, but the ListBoxes have been modified to animate when selected instead of highlighting.

The ListBoxes use the same template shown here:

<DataTemplate x:Key="ListDataTemplate">
    <StackPanel Orientation="Horizontal">
        <TextBlock x:Name="txtName" FontSize="12" Margin="5,5,5,5" Text="{Binding}" TextWrapping="Wrap"  Foreground="#FFFFFF" />
        <Button x:Name="btnRemove" Margin="0,-15,5,0" VerticalAlignment="Center"  Height="10" Opacity="0.0" Tag="{Binding}" Cursor="Hand">
            <Button.Template>
                <ControlTemplate>
                    <TextBlock Text="r" Foreground="#a7a7a7" FontFamily="Webdings"/>
                </ControlTemplate>
            </Button.Template>
        </Button>
    </StackPanel>

    <DataTemplate.Triggers>
    	<!--WHEN SELECTED-->
    	<DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}, Mode=FindAncestor}}" Value="True">
    		<DataTrigger.EnterActions>
    			<BeginStoryboard>
    				<!--MAKE FONT GROW-->
    				<Storyboard Storyboard.TargetName="txtName" Storyboard.TargetProperty="FontSize">
    					<DoubleAnimation From="12" To="22" Duration="0:0:0.5"/>
    				</Storyboard>
    			</BeginStoryboard>
    			<BeginStoryboard>
    				<!--MAKE HIDDEN TOOLBAR APPEAR-->
    				<Storyboard Storyboard.TargetName="btnRemove" Storyboard.TargetProperty="(UIElement.Opacity)">
    					<DoubleAnimationUsingKeyFrames BeginTime="00:00:0.2">
    						<SplineDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
    						<SplineDoubleKeyFrame KeyTime="00:00:01" Value="1"/>
    					</DoubleAnimationUsingKeyFrames>
    				</Storyboard>
    			</BeginStoryboard>
    		</DataTrigger.EnterActions>
    		<DataTrigger.ExitActions>
    			<BeginStoryboard>
    				<!--SHRINK FONT BACK TO ORIGINAL SIZE-->
    				<Storyboard Storyboard.TargetName="txtName" Storyboard.TargetProperty="FontSize">
    					<DoubleAnimation From="22" To="12" Duration="0:0:0.5"/>
    				</Storyboard>
    			</BeginStoryboard>
    			<BeginStoryboard>
    				<!--HIDE TOOLBAR AGAIN-->
    				<Storyboard Storyboard.TargetName="btnRemove" Storyboard.TargetProperty="(UIElement.Opacity)">
    					<DoubleAnimationUsingKeyFrames BeginTime="00:00:00">
    						<SplineDoubleKeyFrame KeyTime="00:00:00" Value="1"/>
    						<SplineDoubleKeyFrame KeyTime="00:00:01" Value="0"/>
    					</DoubleAnimationUsingKeyFrames>
    				</Storyboard>
    			</BeginStoryboard>
    		</DataTrigger.ExitActions>
    	</DataTrigger>

    </DataTemplate.Triggers>
</DataTemplate>

You can see the animations based on the triggers when items are selected, and the remove button (the X, it’s the WingDings font “r”) is also displayed by changing it’s opacity. One thing to note is that the btnRemove has the tag property set to the binding. I’ll cover this below in more detail but it’s needed to determine what item the user is working with.

Each ListBox is shown below. lstRoots is the first ListBox that gets the root words, and lstResults is the ListBox that gets the newly created words.

<Grid Background="{x:Null}" Grid.ColumnSpan="1" Margin="8,36,8,12" >
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="269.105"/>
        <ColumnDefinition/>
    </Grid.ColumnDefinitions>

    <Grid x:Name="grdRoot" Height="Auto" Margin="0,0,11.5,0">
        <TextBlock Text="roots" Foreground="White" FontSize="26.667" HorizontalAlignment="Center" FontFamily="Segoe Condensed" Height="29" VerticalAlignment="Top"/>
        <TextBox x:Name="txtRoot" Background="Transparent" KeyUp="txtRoot_KeyUp" Foreground="White" Height="27" VerticalAlignment="Top" Margin="8,29,8,0" />

        <ListBox x:Name="lstRoots" Background="{x:Null}" BorderBrush="{x:Null}" Foreground="#FFFFFFFF" Width="Auto" Height="Auto" SelectionMode="Multiple" Margin="0,69,0,0"
        	ItemsSource="{Binding Roots}" ItemTemplate="{StaticResource ListDataTemplate}" SelectionChanged="lstRoot_SelectionChanged" ButtonBase.Click="RemoveRootWord">
        </ListBox>
    </Grid>

    <Grid x:Name="grdResults" Height="Auto" Margin="8.5,0,10,0" Grid.Column="1">
        <TextBlock Text="results" Foreground="White" FontSize="26.667" HorizontalAlignment="Center" FontFamily="Segoe Condensed" Height="28" VerticalAlignment="Top"/>

        <ListBox x:Name="lstResults" Background="{x:Null}" BorderBrush="#FF979797" Foreground="#FFFFFFFF" Width="Auto" Height="Auto" SelectionMode="Multiple" Margin="0,70,0,0"
        	ItemsSource="{Binding Path=ResultsViewSource.View}" ItemTemplate="{StaticResource ListDataTemplate}" ButtonBase.Click="RemoveResultWord" BorderThickness="1,0,0,0"  >
            <ListBox.ItemsPanel>
                <ItemsPanelTemplate>
                    <!--Work around for wrapping issue see http://bit.ly/q7kdZV>-->
                    <WrapPanel IsItemsHost="True" Width="{Binding Path=ActualWidth,RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ScrollContentPresenter}}}"/>
                </ItemsPanelTemplate>
            </ListBox.ItemsPanel>
        </ListBox>
        <CheckBox x:Name="chkMerged" Content="Merged" HorizontalAlignment="Left" Height="23" VerticalAlignment="Top" Width="72" Margin="8,31,0,0"
                    Foreground="White" IsChecked="True" Checked="WordTypeFiltersChanged" Unchecked="WordTypeFiltersChanged"/>

        <CheckBox x:Name="chkPrefix" Content="Prefixes" Height="23" VerticalAlignment="Top" Margin="80,31,0,0"
                    Foreground="White" HorizontalAlignment="Left" Width="61.895" Checked="WordTypeFiltersChanged" Unchecked="WordTypeFiltersChanged"/>

        <CheckBox x:Name="chkSuffix" Content="Suffixes" Height="23" VerticalAlignment="Top" Margin="145.895,31,0,0"
                    Foreground="White" HorizontalAlignment="Left" Width="72" d:LayoutOverrides="HorizontalAlignment" Checked="WordTypeFiltersChanged" Unchecked="WordTypeFiltersChanged"/>
    </Grid>
</Grid>

Note that each ListBox hooks the RoutedEvent for the ButtonBase.Click? Since the button is in the template and used for both lists I didn’t want to call an event from it or we would also need to determine which list was the caller. This way we can respond to the event easier and it keeps the template clean.

When a result is generated from a word it can match one of three items:

  • a merged word
  • used a prefix
  • used a suffix

This is necessary because of the filters (CheckBoxes). When a filter is selected we need to know how the word was generated so we can filter it. So I created a small Word class:

namespace WordMerge
{
    public class Word
    {
        public enum WordTypeEnum
        {
            Merged,
            Prefix,
            Suffix
        }

        public string Value { get; set; }
        public WordTypeEnum WordType { get; set; }

        public Word()
        {}

        public Word(string value, WordTypeEnum type)
        {
            Value = value;
            WordType = type;
        }

        public override string ToString()
        {
            return Value;
        }

        public bool Equals(Word other)
        {
            return Equals(other.Value, Value);
        }

        public override bool Equals(object obj)
        {
            if (obj.GetType() != typeof(Word)) return false;
            return Equals((Word) obj);
        }

        public override int GetHashCode()
        {
            return (Value != null ? Value.GetHashCode() : 0);
        }
    }
}

The Word class is a simple state bag that contains a word and flag to let the system know how it was generated.

In the code behind the Roots and Results are stored. A CollectionViewSource is used on the Results collection so it can be easily filtered when a CheckBox is clicked.

public ObservableCollection<string> Roots { get; set; }
private ObservableCollection<Word> results { get; set; }
public CollectionViewSource ResultsViewSource { get; set; }

The filter code uses the WordTypeEnum on the Word class:

private void ResultsViewSource_Filter(object sender, FilterEventArgs e)
{
    e.Accepted = true;

    Word word = (Word)e.Item;

    switch(word.WordType)
    {
        case Word.WordTypeEnum.Merged:
            if (chkMerged.IsChecked == false) e.Accepted = false;
            break;

        case Word.WordTypeEnum.Prefix:
            if (chkPrefix.IsChecked == false) e.Accepted = false;
            break;

        case Word.WordTypeEnum.Suffix:
            if (chkSuffix.IsChecked == false) e.Accepted = false;
            break;
    }
}

When a new word is entered into the TextBox this code is executed:

private void txtRoot_KeyUp(object sender, KeyEventArgs e)
{
    if (e.Key != Key.Enter) return;
    if (string.IsNullOrEmpty(txtRoot.Text)) return;

    string input = txtRoot.Text.Trim();

    txtRoot.Text = "";

    if (Roots.Contains(input)) return;

    //Build Merged words
    foreach (string root in Roots)
    {
        results.Insert(0, new Word(input + root,Word.WordTypeEnum.Merged));
        results.Insert(0, new Word(root + input, Word.WordTypeEnum.Merged));
    }

    //Build Prefixes
    foreach (string prefix in prefixes)
    {
        Word word = new Word(prefix + input,Word.WordTypeEnum.Prefix);

        if (results.Any(w => w.Value == word.Value)) continue;

        results.Insert(0, word);
    }

    //Build Suffixes
    foreach (string suffix in suffixes)
    {
        Word word = new Word(suffix + input, Word.WordTypeEnum.Suffix);

        if (results.Any(w => w.Value == word.Value)) continue;

        results.Insert(0, word);
    }

    Roots.Insert(0, input);
}

The suffixes and prefixes are simple text files that sit in the application folder. These are loaded in the constructor of the window.

private List<string> suffixes;
private List<string> prefixes;

public MainWindow()
        {
            ...

            prefixes = new List<string>(File.ReadAllLines("prefixes.txt"));
            suffixes = new List<string>(File.ReadAllLines("suffixes.txt"));
        }

When a word is selected in the lstRoots ListBox the system searches the lstResults ListBox for words that contain the root word, when they are found they are selected.

private void lstRoot_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    lstResults.SelectedItems.Clear();

    if (lstRoots.SelectedItems.Count > 1) lstRoots.SelectedItems.RemoveAt(0);

    string word = (string) lstRoots.SelectedItem;

    SelectAncestorWords(word);
}

private void SelectAncestorWords(string baseWord)
{
    if (baseWord == null) return;

    foreach (Word word in results)
    {
        if (!word.Value.Contains(baseWord)) continue;
        if (lstResults.SelectedItems.Contains(word)) continue;
        lstResults.SelectedItems.Add(word);
    }
}

And when the “X” button is clicked to remove an item from a list this code occurs:

private void RemoveRootWord(object sender, RoutedEventArgs e)
{
    string word = (string) ((FrameworkElement) e.OriginalSource).Tag;

    for (int i = results.Count - 1; i >= 0; i--)
    {
        if (results[i].Value.Contains(word)) results.RemoveAt(i);
    }

    Roots.Remove(word);
}

private void RemoveResultWord(object sender, RoutedEventArgs e)
{
    Word word = (Word) ((FrameworkElement) e.OriginalSource).Tag;
    results.Remove(word);
}

Notice in that code for removal we access the Tag. Remember earlier in the XAML we bound the current item to the tag property? Now we can retrieve it easily and know which item the user clicked in the list.

Modifying the selection in a WPF ListBox is simple enough to do but as you see can change the feeling of the application. Imagine if I just left the app with the default highlight on selection? Would the user experience be the same? Obviously not, the little things such as this can really resonate with your users. I hope you enjoyed this project and can find a use for this in your code.

Leave a Comment

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>