Setting a Custom WPF ToolTip and how to keep it shown…

Ok, so to be perfectly honest I was a little bored at work and decided to experiment with my super duper omega wonder app (now with kitchen sink features).  My experiment was to put some ToolTip pop ups of historical data about whatever cell the mouse happened to bre hovering over…so I started whipping up a test app to figure out how to do that…and you the lucky reader can follow my journey.  I should be charging for this, I know but we can chalk it up to my humanitarian contribution to our species for now.  *drip drip drip of sarcasim*

Ok, so in our test app we need a WPF window with a basic generic grid on it.  I’ve been using Telerik WPF controls, but everything I do in this post should work just as well with standard .NET WPF controls as well.  Telerik isn’t doing anything special to ToolTips and AFAIK they just expose the underlying .NET stuff.  Anyway, my examples will all be with Telerik…so take it FWIW.

First I’m going to make up some ficticious relational data.  It’s totally meaningless but will be useful to test things with.  So I’m going to be tracking scores of players who participate in multiple sports.  Here are the classes I’ve defined to store the data.

Player class:

public class Player
{
    public string UniqueName { get; set; }

    public string SportName { get; set; }

    public string Score { get; set; }

    public DateTime AchievedDate { get; set; }
}

SportContest class and the classes inheriting from it:

public class SportContest
{
    public List<Player> Contestants { get; set; }

    public IEnumerable<Player> GetResults()
    {
        if (Contestants == null)
        {
            return null;
        }
        else
        {
            return Contestants.Where(c => c.SportName == this.GetType().Name);
        }
    }
}

public class Hockey : SportContest
{
    public void AddScore(string playerUniqueName)
    {
        if(Contestants == null)
        {
            Contestants = new List<Player>();
        }

        Player player = Contestants.FirstOrDefault(c =>
            c.UniqueName == playerUniqueName &&
            c.SportName == this.GetType().Name);

        if (player == null)
        {
            player = new Player();
            player.UniqueName = playerUniqueName;
            player.SportName = this.GetType().Name;
            player.Score = "1 goal";
            player.AchievedDate = DateTime.Now;

            Contestants.Add(player);
        }
        else
        {
            int scoreValue = Convert.ToInt32(player.Score.Substring(0, player.Score.IndexOf(" ")));
            player.Score = $"{++scoreValue} goals";
        }
    }
}

public class Baseball : SportContest
{
    public void AddScore(string playerUniqueName)
    {
        if (Contestants == null)
        {
            Contestants = new List<Player>();
        }

        Player player = Contestants.FirstOrDefault(c =>
            c.UniqueName == playerUniqueName &&
            c.SportName == this.GetType().Name);

        if (player == null)
        {
            player = new Player();
            player.UniqueName = playerUniqueName;
            player.SportName = this.GetType().Name;
            player.Score = "1 run";
            player.AchievedDate = DateTime.Now;

            Contestants.Add(player);
        }
        else
        {
            int scoreValue = Convert.ToInt32(player.Score.Substring(0, player.Score.IndexOf(" ")));
            player.Score = $"{++scoreValue} runs";
        }
    }
}

public class Football : SportContest
{
    public void AddScore(string playerUniqueName, int pointsScored)
    {
        if (Contestants == null)
        {
            Contestants = new List<Player>();
        }

        Player player = Contestants.FirstOrDefault(c =>
            c.UniqueName == playerUniqueName &&
            c.SportName == this.GetType().Name);

        if (player == null)
        {
            player = new Player();
            player.UniqueName = playerUniqueName;
            player.SportName = this.GetType().Name;
            player.Score = $"{pointsScored} {(pointsScored > 1 ? " points" : " point")}";
            player.AchievedDate = DateTime.Now;

            Contestants.Add(player);
        }
        else
        {
            int scoreValue = Convert.ToInt32(player.Score.Substring(0, player.Score.IndexOf(" ")));
            scoreValue = scoreValue + pointsScored;
            player.Score = $"{scoreValue} points";
        }
    }
}

public class Basketball : SportContest
{
    public void AddScore(string playerUniqueName, int pointsScored)
    {
        if (Contestants == null)
        {
            Contestants = new List<Player>();
        }

        Player player = Contestants.FirstOrDefault(c =>
            c.UniqueName == playerUniqueName &&
            c.SportName == this.GetType().Name);

        if (player == null)
        {
            player = new Player();
            player.UniqueName = playerUniqueName;
            player.SportName = this.GetType().Name;
            player.Score = $"{pointsScored} {(pointsScored > 1 ? " points" : " point")}";
            player.AchievedDate = DateTime.Now;

            Contestants.Add(player);
        }
        else
        {
            int scoreValue = Convert.ToInt32(player.Score.Substring(0, player.Score.IndexOf(" ")));
            scoreValue = scoreValue + pointsScored;
            player.Score = $"{scoreValue} points";
        }
    }
}

SportResults class:

public class SportResults
{
    public SportResults()
    {
        Contestants = new List<Player>();

        FillHockeyResults();
        FillBaseballResults();
        FillFootballResults();
        FillBasketballResults();
    }

    public List<Player> Contestants { get; set; }

    public Hockey Hockey { get; set; }

    public Baseball Baseball { get; set; }

    public Football Football { get; set; }

    public Basketball Basketball { get; set; }

    public void FillHockeyResults()
    {
        Random rnd = new Random(DateTime.Now.Millisecond);

        Hockey = new Hockey();
        Hockey.Contestants = Contestants;

        for (int i = 0; i < rnd.Next(); i++)
        {
            Hockey.AddScore("Albert");
        }

        for(int i = 0; i < rnd.Next(); i++)
        {
            Hockey.AddScore("Beau");
        }

        for (int i = 0; i < rnd.Next(); i++)
        {
            Hockey.AddScore("Charles");
        }

        for (int i = 0; i < rnd.Next(); i++)
        {
            Hockey.AddScore("David");
        }

        for (int i = 0; i < rnd.Next(); i++)
        {
            Hockey.AddScore("Eugene");
        }
    }

    public void FillBaseballResults()
    {
        Random rnd = new Random(DateTime.Now.Millisecond);

        Baseball = new Baseball();
        Baseball.Contestants = Contestants;

        for (int i = 0; i < rnd.Next(); i++)
        {
            Baseball.AddScore("Beau");
        }

        for (int i = 0; i < rnd.Next(); i++)
        {
            Baseball.AddScore("Charles");
        }

        for (int i = 0; i < rnd.Next(); i++)
        {
            Baseball.AddScore("David");
        }

        for (int i = 0; i < rnd.Next(); i++)
        {
            Baseball.AddScore("Eugene");
        }

        for (int i = 0; i < rnd.Next(); i++)
        {
            Baseball.AddScore("Farley");
        }
    }

    public void FillFootballResults()
    {
        Random rndTimesScored = new Random(DateTime.Now.Millisecond);
        Random rndPointsScored = new Random(DateTime.Now.Millisecond);

        Football = new Football();
        Football.Contestants = Contestants;

        for (int i = 0; i < rndTimesScored.Next(); i++)
        {
            Football.AddScore("Charles", rndPointsScored.Next(1, 7));
        }

        for (int i = 0; i < rndTimesScored.Next(); i++)
        {
            Football.AddScore("David", rndPointsScored.Next(1, 7));
        }

        for (int i = 0; i < rndTimesScored.Next(); i++)
        {
            Football.AddScore("Eugene", rndPointsScored.Next(1, 7));
        }

        for (int i = 0; i < rndTimesScored.Next(); i++)
        {
            Football.AddScore("Farley", rndPointsScored.Next(1, 7));
        }

        for (int i = 0; i < rndTimesScored.Next(); i++)
        {
            Football.AddScore("Godrick", rndPointsScored.Next(1, 7));
        }
    }

    public void FillBasketballResults()
    {
        Random rndTimesScored = new Random(DateTime.Now.Millisecond);
        Random rndPointsScored = new Random(DateTime.Now.Millisecond);

        Basketball = new Basketball();
        Basketball.Contestants = Contestants;

        for (int i = 0; i < rndTimesScored.Next(); i++)
        {
            Basketball.AddScore("David", rndPointsScored.Next(1, 3));
        }

        for (int i = 0; i < rndTimesScored.Next(); i++)
        {
            Basketball.AddScore("Eugene", rndPointsScored.Next(1, 3));
        }

        for (int i = 0; i < rndTimesScored.Next(); i++)
        {
            Basketball.AddScore("Farley", rndPointsScored.Next(1, 3));
        }

        for (int i = 0; i < rndTimesScored.Next(); i++)
        {
            Basketball.AddScore("Godrick", rndPointsScored.Next(1, 3));
        }

        for (int i = 0; i < rndTimesScored.Next(); i++)
        {
            Basketball.AddScore("Harry", rndPointsScored.Next(1, 3));
        }
    }
}

There…if you use the above code…all you have to do is instantiate a new SportResults object and you’ll have a bunch of ficticious data with some relational data between the players.  Now to display that data.  I whipped up a pretty basic WPF app with a RadTabControl and some RadGridViews to display the data.  Here’s what my MainWindow.xaml \ cs files look like as well as the custom user control I created to display results for reach sport.

SportResultsUserControl XAML:

<UserControl    d:DesignHeight="300" 
                d:DesignWidth="300" 
                mc:Ignorable="d" 
                x:Class="ToolTipinator.PlayerStatsUserControl"
                xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
                xmlns:local="clr-namespace:ToolTipinator"
                xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
                xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <telerik:RadGridView    AutoGenerateColumns="False" 
                            ColumnWidth="*" 
                            x:Name="PlayerStatsRadGridView">
        <telerik:RadGridView.Columns>
            <telerik:GridViewDataColumn DataMemberBinding="{Binding SportName}" 
                                        Header="Sport" />
            <telerik:GridViewDataColumn DataMemberBinding="{Binding Score}" 
                                        Header="Score" />
            <telerik:GridViewDataColumn DataMemberBinding="{Binding AchievedDate}" 
                                        Header="Achieved Date" />
        </telerik:RadGridView.Columns>
    </telerik:RadGridView>
</UserControl>

SportResultsUserControl CS:

public partial class SportResultsUserControl : UserControl
{
    public SportResultsUserControl(IEnumerable&amp;lt;Player&amp;gt; contestants)
    {
        InitializeComponent();

        SportResultsRadGridView.ItemsSource = contestants;
    }
}

MainWindow XAML:

<Window Height="350" 
        Width="525" 
        Title="MainWindow" 
        x:Class="ToolTipinator.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Grid>
        <telerik:RadTabControl  Margin="5, 5, 5, 5" 
                                x:Name="ToolTipinatorTabControl">
            <telerik:RadTabItem BorderBrush="Black" 
                                BorderThickness="1" 
                                Header="Hockey" 
                                x:Name="HockeyRadTabItem" />
            <telerik:RadTabItem BorderBrush="Black" 
                                BorderThickness="1" 
                                Header="Baseball" 
                                x:Name="BaseballRadTabItem" />
            <telerik:RadTabItem BorderBrush="Black" 
                                BorderThickness="1" 
                                Header="Football" 
                                x:Name="FootballRadTabItem" />
            <telerik:RadTabItem BorderBrush="Black" 
                                BorderThickness="1" 
                                Header="Basketball" 
                                x:Name="BasketballRadTabItem" />
        </telerik:RadTabControl>
    </Grid>
</Window>

MainWindow CS:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        SportResults = new SportResults();

        ((RadTabItem)ToolTipinatorTabControl.Items[0]).Content = new SportResultsUserControl(SportResults.Hockey.GetResults());
        ((RadTabItem)ToolTipinatorTabControl.Items[1]).Content = new SportResultsUserControl(SportResults.Baseball.GetResults());
        ((RadTabItem)ToolTipinatorTabControl.Items[2]).Content = new SportResultsUserControl(SportResults.Football.GetResults());
        ((RadTabItem)ToolTipinatorTabControl.Items[3]).Content = new SportResultsUserControl(SportResults.Basketball.GetResults());
    }

    public SportResults SportResults { get; set; }
}

Ok…now we should all have a spiffy little WPF app that has 4 tabs one for each sport of Hockey, Baseball, Football, and Basketball.

You may notice that David and Eugene have played in each sport…and some other’s have played in 2 or 3 different sports while some have only played in a single sport.  That’s where the relational data comes in and what I’ll be using in my ToolTip.

What I’m going to do next is set it up so when you hover over a player’s name, you’ll get a ToolTip popup that will display their scores in other sports.  Pretty short and sweet, but it gives a good example of how to set it up.  To do this I’m going to make a custom user control just for the ToolTip.

PlayerStatsUserControl XAML:

<UserControl    d:DesignHeight="300" 
                d:DesignWidth="300" 
                mc:Ignorable="d" 
                x:Class="ToolTipinator.PlayerStatsUserControl"
                xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
                xmlns:local="clr-namespace:ToolTipinator"
                xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
                xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <telerik:RadGridView    AutoGenerateColumns="False" 
                            ColumnWidth="*" 
                            x:Name="PlayerStatsRadGridView">
        <telerik:RadGridView.Columns>
            <telerik:GridViewDataColumn DataMemberBinding="{Binding SportName}" 
                                        Header="Sport" />
            <telerik:GridViewDataColumn DataMemberBinding="{Binding Score}" 
                                        Header="Score" />
            <telerik:GridViewDataColumn DataMemberBinding="{Binding AchievedDate}" 
                                        Header="Achieved Date" />
        </telerik:RadGridView.Columns>
    </telerik:RadGridView>
</UserControl>

PlayerStatsUserControl CS:

public partial class PlayerStatsUserControl : UserControl
{
    public PlayerStatsUserControl(IEnumerable&amp;lt;Player&amp;gt; contestantResults)
    {
        InitializeComponent();

        PlayerStatsRadGridView.ItemsSource = contestantResults;
    }
}

Nothing overly complex.  The magic happens in a new event handler that will need to be added to the SportResultsUserControl for RowLoaded.  I also added an argument to the constructor to set the sport name for the SportResults object.  Here’s what my CS for my SportResultsUserControl looks like now.

SportResultsRadGridView CS:

public partial class SportResultsUserControl : UserControl
{
    public SportResultsUserControl(string sportName, IEnumerable&amp;lt;Player&amp;gt; contestants)
    {
        InitializeComponent();

        if (DesignerProperties.GetIsInDesignMode(this))
            return;

        SportName = sportName;
        SportResultsRadGridView.ItemsSource = contestants;
    }

    public string SportName { get; set; }

    private void SportResultsRadGridView_RowLoaded(object sender, RowLoadedEventArgs e)
    {
        GridViewRow currentRow = null;

        if (e.Row.GetType() == typeof(GridViewRow))
        {
            currentRow = (GridViewRow)e.Row;
        }

        if (currentRow != null)
        {
            var nameCell = currentRow.Cells.FirstOrDefault(c =&gt; c.Column.UniqueName == "UniqueName");

            if (nameCell != null)
            {
                string cellValue = ((GridViewCell)nameCell).Value.ToString();

                if (cellValue != null)
                {
                    IEnumerable&lt;Player&gt; contestantResults = MainWindow.SportResults.Contestants.Where(c =&gt; c.UniqueName == cellValue &amp;&amp; c.SportName != SportName);
                    PlayerStatsUserControl playerStatsToolTip = new PlayerStatsUserControl(contestantResults);
                    playerStatsToolTip.Width = 800;
                    nameCell.ToolTip = playerStatsToolTip;
                }
            }
        }
    }
}

In order for the above to work, I also had to turn the SportResults property that was a memeber of MainWindow into a static property so I could access it elsewhere as well as adding in the sport names to the constructor calls for the SportResultsUserControl…again, here’s what the CS for my MainWindow looks like now…

MainWindow CS:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        ToolTipService.ShowDurationProperty.OverrideMetadata(
            typeof(DependencyObject), new FrameworkPropertyMetadata(Int32.MaxValue));

        MainWindow.SportResults = new SportResults();

        ((RadTabItem)ToolTipinatorTabControl.Items[0]).Content = new SportResultsUserControl(SportResults.Hockey.GetType().Name, SportResults.Hockey.GetResults());
        ((RadTabItem)ToolTipinatorTabControl.Items[1]).Content = new SportResultsUserControl(SportResults.Baseball.GetType().Name, SportResults.Baseball.GetResults());
        ((RadTabItem)ToolTipinatorTabControl.Items[2]).Content = new SportResultsUserControl(SportResults.Football.GetType().Name, SportResults.Football.GetResults());
        ((RadTabItem)ToolTipinatorTabControl.Items[3]).Content = new SportResultsUserControl(SportResults.Basketball.GetType().Name, SportResults.Basketball.GetResults());
    }

    public static SportResults SportResults { get; set; }
}

You may notice the call to ToolTipService in the MainWindow constructor as well now.  This will cause the ToolTip to stay open for about 47 days…so it’s not forever, but I’m pretty sure if a user cannot get the info they need out of a ToolTip within 47 days you have bigger problems with your app.  😉

Ok…that’s it for today’s journey.  Thanks for reading and as always I hope this helps someone.  Enjoy!  🙂

Advertisements
Setting a Custom WPF ToolTip and how to keep it shown…

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s