Delay's Blog

Silverlight, WPF, Windows Phone, Web Platform, .NET, and more...

Posts
  • Delay's Blog

    A quick update to address some top customer issues [AJAX Control Toolkit update!]

    Just a few moments ago we made available the 10201 release of the AJAX Control Toolkit to address some of the most significant issues reported since the 10123 release last week. We chose these issues based on feedback from our users in our support forum and online issue tracker and tried to prioritize issues with widespread benefits while minimizing the risks of breaking anything.

    In particular, we:

    • Fixed problems that occurred when using the new AutoComplete or Tabs controls inside an UpdatePanel
    • Improved the localization behavior of the Calendar so that it works better in other countries/time zones
    • Simplified and improved the implementation of the new "draggable popup" feature of ModalPopup
    • Addressed a TextBoxWatermark focus issue that was inconveniencing users

    Recall that you can sample any of the controls right now (no install required). You can also browse the project web site, download the latest Toolkit, and start creating your own controls and/or contributing to the project!

    If you have any feedback, please share it with us on the support forum (or email me)!

  • Delay's Blog

    Night of the Living WM_DEADCHAR [How to: Avoid problems with international keyboard layouts when hosting a WinForms TextBox in a WPF application]

    There was a minor refresh for the Microsoft Web Platform last week; you can find a list of fixes and instructions for upgrading in this forum post. The most interesting change for the WPF community is probably the fix for a WebMatrix problem brought to my attention by @radicalbyte and @jabroersen where Ctrl+C, Ctrl+X, and Ctrl+V would suddenly stop working when an international keyboard layout was in use. Specifically, once one of the prefix keys (ex: apostrophe, tilde, etc.) was used to type an international character in the editor, WebMatrix's copy/cut/paste shortcuts would stop working until the application was restarted. As it happens, the Ribbon buttons for copy/cut/paste continued to work correctly - but the loss of keyboard shortcuts was pretty annoying. :(

    Fortunately, the problem was fixed! This is the story of how...

     

    To begin with, it's helpful to reproduce the problem on your own machine. To do that, you'll need to have one of the international keyboard layouts for Windows installed. If you spend a lot of time outside the United States, you probably already do, but if you're an uncultured American like me, you'll need to add one manually. Fortunately, there's a Microsoft Support article that covers this very topic: How to use the United States-International keyboard layout in Windows 7, in Windows Vista, and in Windows XP! Not only does it outline the steps to enable an international keyboard layout, it also explains how to use it to type some of the international characters it is meant to support.

    Aside: Adding a new keyboard layout is simple and unobtrusive. The directions in the support article explain what to do, though I'd suggest skipping the part about changing the "Default input language": if you stop at adding the new layout, then your default experience will remain the same and you'll be able to selectively opt-in to the new layout on a per-application basis.

    With the appropriate keyboard layout installed, the other thing you need to reproduce the problem scenario is a simple, standalone sample application, so...

    [Click here to download the InternationalKeyboardBugWithWpfWinForms sample application.]

     

    Compiling and running the sample produces a .NET 4 WPF application with three boxes for text. The first is a standard WPF TextBox and it works just like you'd expect. The second is a standard Windows Forms TextBox (wrapped in a WindowsFormsHost control) and it demonstrates the problem at hand. The third is a trivial subclass of that standard Windows Forms TextBox that includes a simple tweak to avoid the problem and behave as you'd expect. Here's how it looks with an international keyboard layout selected:

    InternationalKeyboardBugWithWpfWinForms sample

    Although the sample application doesn't reproduce the original problem exactly, it demonstrates a related issue caused by the same underlying behavior. To see the problem in the sample application, do the following:

    1. Make the InternationalKeyboardBugWithWpfWinForms window active.
    2. Switch to the United States-International keyboard layout (or the corresponding international keyboard layout for your country).
    3. Press Ctrl+O and observe the simple message box that confirms the system-provided ApplicationCommands.Open command was received.
    4. Type one of the accented characters into the WinForms TextBox (the middle one).
    5. Press Ctrl+O again and notice that the message box is no longer displayed (and the Tab key has stopped working).

     

    As you can see, the InternationalTextBox subclass avoids the problem its WinForms TextBox base class exposes. But how? Well, rather simply:

    /// <summary>
    /// Trivial subclass of the Windows Forms TextBox to work around problems
    /// that occur when hosted (by WindowsFormsHost) in a WPF application.
    /// </summary>
    public class InternationalTextBox : System.Windows.Forms.TextBox
    {
        /// <summary>
        /// WM_DEADCHAR message value.
        /// </summary>
        private const int WM_DEADCHAR = 0x103;
    
        /// <summary>
        /// Preprocesses keyboard or input messages within the message loop
        /// before they are dispatched.
        /// </summary>
        /// <param name="msg">Message to pre-process.</param>
        /// <returns>True if the message was processed.</returns>
        public override bool PreProcessMessage(ref Message msg)
        {
            if (WM_DEADCHAR == msg.Msg)
            {
                // Mark message as handled; do not pass it on
                return true;
            }
    
            // Call base class implementation
            return base.PreProcessMessage(ref msg);
        }
    }

    The underlying problem occurs when the WM_DEADCHAR message is received and processed by both WPF and WinForms, so the InternationalTextBox subclass shown above prevents that by intercepting the message in PreProcessMessage, marking it handled (so nothing else will process it), and skipping further processing. Because the underlying input-handling logic that actually maps WM_DEADCHAR+X to an accented character still runs, input of accented characters isn't affected - but because WPF doesn't get stuck in its WM_DEADCHAR mode, keyboard accelerators like Ctrl+O continue to be processed correctly!

    Aside: The change above is not exactly what went into WebMatrix... However, this change appears to work just as well and is a bit simpler, so I've chosen to show it here. The actual change is similar, but instead of returning true, maps the WM_DEADCHAR message to a WM_CHAR message (ex: msg.MSG = WM_CHAR;) and allows the normal base class processing to handle it. I'm not sure the difference matters in most scenarios, but it's what appeared to be necessary at the time, it's what the QA team signed off on, and it's what the WPF team reviewed. :) I'm optimistic the simplified version I show here will work well everywhere, but if you find somewhere it doesn't, please try the WM_CHAR translation instead. (And let me know how it works out for you!)

     

    Hosting Windows Forms controls in a WPF application isn't the most common thing to do, but sometimes it's necessary (or convenient!), so it's good to ensure everything works well together. If your application exposes any TextBox-like WinForms classes, you might want to double-check that things work well with an international keyboard layout. And if they don't, a simple change like the one I show here may be all it takes to resolve the problem! :)

    PS - I mention above that the WPF team had a look at this workaround for us. They did more than that - they fixed the problem for the next release of WPF/WinForms so the workaround will no longer be necessary! Then QA checked to make sure an application with the workaround wouldn't suddenly break when run on a version of .NET/WPF/WinForms with the actual fix - and it doesn't! So apply the workaround today if you need it - or wait for the next release of the framework to get it for free. Either way, you're covered. :)
  • Delay's Blog

    64 bits ought to be enough for everybody [TextAnalysisTool.NET update for .NET 2.0 and 64-bit enables the analysis of larger files!]

    • 12 Comments

    TextAnalysisTool.NET is a free program designed to excel at viewing, searching, and navigating large files quickly and efficiently.

    I wrote the first version of TextAnalysisTool back in 2000 using C++ and Win32. In 2003, I rewrote it using the new .NET 1.0 Framework - and upgraded to .NET 1.1 later that year. There were a variety of improvements between then and 2006, the date of the last update to TextAnalysisTool.NET. Along the way, I've heard from a lot of people who use this tool to simplify their daily workflow! In the past year, I've started getting requests for 64-bit support from folks working with extremely large files that don't fit in the 4GB virtual address space limits of a normal 32-bit process on Windows. Although .NET 1.1 didn't support 64-bit processes, .NET 2.0 does, and I've decided it's finally time to take the plunge.:)

    Animated GIF showing basic TextAnalysisTool.NET functionality

     

    With this release, TextAnalysisTool.NET has been compiled using the .NET 2.0 toolset and the AnyCPU option which automatically matches a process to the architecture of its host operating system. On 32-bit OSes, TextAnalysisTool.NET will continue to run as a 32-bit process, but on 64-bit OSes, it will run as a 64-bit process and have access to a significantly larger address space. This makes it possible to work with larger log files without the risk of crashing into the 4GB memory limit (which can end up being as low as 1.7 GB in practice)!

    Other than a few exceedingly minor string updates, I have made no changes to the way TextAnalysisTool.NET behaves - so the new version should feel just like the previous one. The framework update means .NET 1.1 is no longer a supported platform and .NET 2.0 is now natively supported. The included .config file allows the same executable to run under .NET 4.0 as-is (for example on Windows 8 without the optional ".NET Framework 3.5" feature installed).

    If you've ever run out of memory using TextAnalysisTool.NET, please give this new version a try! And if not, go ahead and continue using the previous version without worrying that you're missing out on anything.:)

     

    Click here to download the latest version of TextAnalysisTool.NET

    Click here to visit the TextAnalysisTool.NET web page for more information

     

    Many thanks to everyone for all the great feedback - I love getting messages from people around the world who are using TextAnalysisTool.NET to make their lives easier!

     

    Aside: As a matter of technical interest, details on the one bug I found with 64-bit TextAnalysisTool.NET: the following code had worked fine for the last decade (message.WParam is an IntPtr via Form.WndProc for WM_MOUSEWHEEL):
    int wheelDelta = ((int)message.WParam)>>16; // HIWORD(WPARAM)
    However, when that assignment ran under .NET 2.0 on a 64-bit OS, it quickly threw OverflowException! I was surprised, but it turns out this is documented behavior. Because that line interoperates with the Windows API, I couldn't change the types involved - but I could tweak the code to avoid the exception by avoiding the problematic explicit conversion:
    int wheelDelta = ((int)((long)message.WParam))>>16; // HIWORD(WPARAM)
    Yep, the proverbial one-line fix saves the day!
  • Delay's Blog

    Going dark [MSDN blogging platform being upgraded - NO new posts or comments next week]

    The administrators of the MSDN blogging platform are performing a software upgrade and will be putting all blogs into read-only mode on Sunday, May 16th. New posts and new comments will not be possible for this blog during the transition. The upgrade is expected to finish by Monday, May 24th - but difficulties during the migration could push that date back. I'll post a quick note once the dust has settled and things are back to normal.

    In the meantime, you should be able to contact me using the old platform's email form. Alternatively, I can be reached on Twitter as @DavidAns.

    Thank you for your patience - see you on the other side! :)

  • Delay's Blog

    So long, and thanks for all the fish! [Moving my blog to a new location - and hosting it on Node.js!]

    After blogging for many years here on MSDN, I'm switching to a custom blog implementation on my own server.

    I know, right?

    Well, it's not as crazy as it sounds; I explain everything in the introductory post.

    Summarizing briefly: All future posts/comments happen on the new blog. This blog is now read-only.

    Those of you interested in continuing the journey will please join me at "The blog of dlaa.me" and/or subscribe to the RSS feed.

    Thank you!

  • Delay's Blog

    Setting a value to null might be more dangerous than you think [Simple ToolTipServiceExtensions class avoids a runtime exception in Windows Store apps]

    I was playing around with a Windows Store app this weekend and ran into a pretty annoying problem. At first, I couldn't tell what was going on; it seemed the app would crash at random times for no apparent reason. But after a bit of debugging to isolate the problem, I figured out what was going on.

    Problem: If you have a ToolTipService.ToolTip data binding in a Windows Store app and the value of that binding transitions from non-null to null while it's being displayed, the next time the tooltip is shown, the platform will throw a NullReferenceException from native code and terminate the application.

    This is a surprisingly severe consequence for something that's likely to happen with some regularity, so you'd expect someone to have run into it before now. And indeed, someone did: tkrasinger reported this problem in November of last year. It's unclear where things went from there, but I've verified the problem still occurs with fully-patched Windows 8 and also with the Windows 8.1 preview released last week.

    At first glance, the situation seems pretty dire because there's no clear way to intercept the exception. And while changing the code to use an empty string instead of null does avoid the crash, it also results in an ugly little white square when you hover. Fortunately, if you know a bit about how tooltips work, you know there's a ToolTip class that gets injected in this scenario to host the bound content. What if we intercepted the binding and made sure to always provide a non-null ToolTip instance? Would that avoid the crash?

    Spoiler alert: It does. :)

     

    There are various ways you might go about implementing this workaround - I chose to use a custom attached property because the result looks the same in XAML and neatly encapsulates all the code in one simple, standalone class.

    Let's say you were using a tooltip like so:

    <Border ToolTipService.ToolTip="{Binding BindingThatCanBecomeNull}">
        <TextBlock Text="Watch out, ToolTipService might crash your app..."/>
    </Border>

    As we've established, if that binding goes null while the user is mouse-ing around, the app is likely to crash soon afterward.

     

    So let's use my ToolTipServiceExtensions class to avoid the problem! First, download ToolTipServiceExtensions.cs from the link below and add it to your Windows Store app project. Next, add the corresponding namespace declaration to the top the XAML:

    xmlns:delay="using:Delay"

    And lastly, tweak the XAML to use ToolTipServiceExtensions instead of ToolTipService:

    <Border delay:ToolTipServiceExtensions.ToolTip="{Binding BindingThatCanBecomeNull}">
        <TextBlock Text="ToolTipServiceExtensions saves the day!"/>
    </Border>

    That's it - you're done! Random crashes from null-going tooltips should be a thing of the past. :)

     

    [Click here to open ToolTipServiceExtensions.cs or right-click/save-as to download it to your machine]

     

    Aside: If you're using any of the other ToolTipService properties in your code, they are unaffected by this change. All ToolTipServiceExtensions does is wrap the content in a ToolTip before deferring to the existing ToolTipService implementation.

     

    For the curious, here's what the code looks like:

    namespace Delay
    {
        /// <summary>
        /// Class containing a replacement for ToolTipService.SetToolTip that works
        /// around a Windows 8 platform bug where NullReferenceException is thrown
        /// from native code the next time a ToolTip is displayed if its Binding
        /// transitions from non-null to null while on screen.
        /// </summary>
        public static class ToolTipServiceExtensions
        {
            /// <summary>
            /// Gets the value of the ToolTipServiceExtensions.ToolTip XAML attached property for an object.
            /// </summary>
            /// <param name="obj">The object from which the property value is read.</param>
            /// <returns>The object's tooltip content.</returns>
            [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Underlying method will validate.")]
            public static object GetToolTip(DependencyObject obj)
            {
                return (object)obj.GetValue(ToolTipProperty);
            }
    
            /// <summary>
            /// Sets the value of the ToolTipServiceExtensions.ToolTip XAML attached property.
            /// </summary>
            /// <param name="obj">The object to set tooltip content on.</param>
            /// <param name="value">The value to set for tooltip content.</param>
            [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Underlying method will validate.")]
            public static void SetToolTip(DependencyObject obj, object value)
            {
                obj.SetValue(ToolTipProperty, value);
            }
    
            /// <summary>
            /// Gets or sets the object or string content of an element's ToolTip.
            /// </summary>
            [SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification = "Stardard attached property implementation.")]
            public static readonly DependencyProperty ToolTipProperty =
                DependencyProperty.RegisterAttached(
                    "ToolTip",
                    typeof(object),
                    typeof(ToolTipServiceExtensions),
                    new PropertyMetadata(null, ToolTipPropertyChangedCallback));
    
            /// <summary>
            /// Method called when the value of an element's ToolTipServiceExtensions.ToolTip XAML attached property changes.
            /// </summary>
            /// <param name="element">Element for which the property changed.</param>
            /// <param name="args">Event arguments.</param>
            private static void ToolTipPropertyChangedCallback(DependencyObject element, DependencyPropertyChangedEventArgs args)
            {
                // Capture the new value
                var newValue = args.NewValue;
    
                // Create a ToolTip instance to display the new value
                var toolTip = new ToolTip { Content = newValue };
    
                // Hide the ToolTip instance if the new value is null
                // (Prevents the display of a small white rectangle)
                if (null == newValue)
                {
                    toolTip.Visibility = Visibility.Collapsed;
                }
    
                // Defer to ToolTipService.SetToolTip for the actual implementation
                ToolTipService.SetToolTip(element, toolTip);
            }
        }
    }
  • Delay's Blog

    Is it still cheating if you come up with the cheat yourself? [Simple code to solve a "sliding pieces" puzzle]

    I was playing around with one of those "rearrange the pieces on the board" puzzles recently and realized I was stumped after unsuccessfully making the same moves over and over and over...:|

    Aside: If you're not familiar with sliding puzzles, the 15-puzzle and Klotski puzzle are both classic examples.

     

    For the particular puzzle I was stuck on, the board looks like:

      ######
     #      #
     #      #
     #      #
    #        #
     #  ##  #
     #  ##  #
      ##  ##

    And uses the following seven pieces:

    AA  BB  CC  DD   E  FF   G
    AA  BB  CC  D   EE   F   GG

    The challenge is to get the 'A' piece from here:

      ######
     #      #
     #      #
     #      #
    #        #
     #AA##  #
     #AA##  #
      ##  ##

    To here:

      ######
     #      #
     #      #
     #      #
    #        #
     #  ##AA#
     #  ##AA#
      ##  ##

    With the starting state:

      ######
     #DDBBFF#
     #DEBBGF#
     #EECCGG#
    #   CC   #
     #AA##  #
     #AA##  #
      ##  ##

    Go ahead and give it a try if you want a challenge! You can make your own puzzle out of paper cutouts or build something with interlocking cubes (which I can say from experience works quite well).

    Aside: I'm not going to reveal where the original puzzle came from because I don't want to spoil it for anyone. But feel free to leave a comment if it looks familiar!

     

    Now, maybe the puzzle's solution is/was obvious to you, but like I said, I was stuck. As it happens, I was in a stubborn mood and didn't want to admit defeat, so I decided to write a simple program to solve the puzzle for me!

    I'd done this before (many years ago), and had a decent sense of what was involved; I managed to bang out the solution below pretty quickly. My goal was to solve the puzzle with minimal effort on my part - so the implementation favors simplicity over performance and there's a lot of room for improvement. Still, it finds the solution in about a second and that's more than quick enough for my purposes.:)

    I've included the complete implementation below. The code should be fairly self-explanatory, so read on if you're interested in one way to solve something like this. Two things worth calling out are that this approach is guaranteed to find the solution with the fewest number of moves and it can handle arbitrarily-shaped pieces and boards - both of which flow pretty naturally from the underlying design.

    There's not much more to say - except that you should feel free to reuse the code for your own purposes!

     

    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Linq;
    
    // Quick (minimally-optimized) solver for a typical "slide the pieces" puzzle.
    // * Implements breadth-first search to guarantee it finds the optimal solution
    // * Detects already-seen states and avoids analyzing them again
    // * Handles arbitrarily shaped pieces and irregular/non-contiguous boards
    // Performance notes:
    // * Board.GetHashCode is called *very* often; keeping it fast is ideal
    // * Board.IsValid is also called frequently and should be quick to run
    // * Board._locations could have the board location removed (it's always 0)
    // * It may be more efficient check validity before creating Board instances
    class SlidePuzzleSolver
    {
        public static void Main()
        {
    #if DEBUG
            // Quick sanity check of the Board class
            var test = new Board();
            Debug.Assert(test.IsValid);
            Debug.Assert(!test.IsSolved);
    #endif
    
            // Stopwatch is handy for measuring/improving performance
            var stopwatch = new Stopwatch();
            stopwatch.Start();
    
            // Initialize
            Board start = new Board();
            Board solved = null;
            var seen = new HashSet<Board>(start); // IEqualityComparer<Board>
            var todo = new Queue<Board>();
            todo.Enqueue(start);
            seen.Add(start);
    
            // Keep going as long as there are unseen states...
            while (0 < todo.Count)
            {
                // Get the next board and process its moves
                var board = todo.Dequeue();
                foreach (var move in board.GetMoves())
                {
                    if (move.IsSolved)
                    {
                        // Solved!
                        solved = move;
                        todo.Clear();
                        break;
                    }
                    if (!seen.Contains(move))
                    {
                        // Enqueue the new state
                        todo.Enqueue(move);
                        seen.Add(move);
                    }
                }
            }
    
            // Write elapsed time to debug window
            stopwatch.Stop();
            Debug.WriteLine("Elapsed time: {0}ms", stopwatch.ElapsedMilliseconds);
    
            // Reverse the solved->start parent chain
            Debug.Assert(null != solved);
            var solution = new Stack<Board>();
            while (null != solved)
            {
                solution.Push(solved);
                solved = solved.Parent;
            }
    
            // Display the solution start->solved
            foreach (var board in solution)
            {
                board.Show();
            }
        }
    
        // Representation of a board and the arrangement of pieces on it
        // (IEqualityComparer<Board> is used by HashSet<Board> above)
        private class Board : IEqualityComparer<Board>
        {
            // Constants for board size
            private const int Width = 10;
            private const int Height = 8;
    
            // Statics for pieces and moves
            private static readonly IReadOnlyList<Piece> _pieces;
            private static readonly IReadOnlyList<int> _deltas;
    
            static Board()
            {
                // Initialize (shared) piece instances
                // Pieces are defined by cells they occupy as deltas from their origin (top-left)
                // Cells are numbered 0..N going from top-left to bottom-right
                // The board is treated as a piece, but never allowed to move
                var pieces = new List<Piece>();
                pieces.Add(new Piece('A', 0, 1, 10, 11)); // Square
                pieces.Add(new Piece('B', 0, 1, 10, 11)); // Square
                pieces.Add(new Piece('C', 0, 1, 10, 11)); // Square
                pieces.Add(new Piece('D', 0, 1, 10));     // 'L'
                pieces.Add(new Piece('E', 1, 10, 11));    // 'L'
                pieces.Add(new Piece('F', 0, 1, 11));     // 'L'
                pieces.Add(new Piece('G', 0, 10, 11));    // 'L'
                pieces.Add(new Piece('#', 2, 3, 4, 5, 6, 7, 11, 18, 21, 28, 31, 38, 40, 49, 51, 54, 55, 58, 61, 64, 65, 68, 72, 73, 76, 77)); // Irregular board shape
                _pieces = pieces.AsReadOnly();
                // Initialize move deltas (each represents one cell left/right/up/down)
                _deltas = new int[] { -1, 1, -Width, Width };
            }
    
            // Piece locations
            private readonly int[] _locations;
    
            // Parent board
            public Board Parent { get; private set; }
    
            // Create starting state of the puzzle
            public Board()
            {
                // Board piece (last element) is always at offset 0
                _locations = new int[] { 52, 14, 34, 12, 22, 16, 26, 0 };
            }
    
            // Create a board from its parent
            private Board(Board parent, int[] locations)
            {
                Parent = parent;
                _locations = locations;
            }
    
            // Get the valid moves from the current board state
            public IEnumerable<Board> GetMoves()
            {
                // Try to move each piece (except for the board)...
                for (var p = 0; p < _pieces.Count - 1; p++)
                {
                    // ... in each direction...
                    foreach (var delta in _deltas)
                    {
                        // ... to create the corresponding board...
                        var locations = (int[])_locations.Clone();
                        locations[p] += delta;
                        var board = new Board(this, locations);
                        // ... and return it if it's valid
                        if (board.IsValid)
                        {
                            yield return board;
                        }
                    }
                }
            }
    
            // Checks whether a board is valid (i.e., has no overlapping cells)
            public bool IsValid
            {
                get
                {
                    // Array to track occupied cells
                    var locations = new bool[Width * Height];
                    // For each piece (including the board)...
                    for (var p = 0; p < _pieces.Count; p++)
                    {
                        var piece = _pieces[p];
                        var offsets = piece.Offsets;
                        // ... for each cell it occupies...
                        for (var o = 0; o < offsets.Length; o++)
                        {
                            // ... check if the cell is occupied...
                            var location = _locations[p] + offsets[o];
                            if (locations[location])
                            {
                                // Already occupied; invalid board
                                return false;
                            }
                            // ... and mark it occupied
                            locations[location] = true;
                        }
                    }
                    return true;
                }
            }
    
            // Checks if the board is solved
            public bool IsSolved
            {
                get
                {
                    // All that matters in *this* puzzle is whether the 'A' piece is at its destination
                    return (56 == _locations[0]);
                }
            }
    
            // Show the board to the user
            public void Show()
            {
                // Clear the console
                Console.Clear();
                // For each piece (including the board)...
                for (var p = 0; p < _pieces.Count; p++)
                {
                    var piece = _pieces[p];
                    // ... for each offset...
                    foreach (var offset in piece.Offsets)
                    {
                        // ... determine the x,y of the cell...
                        var location = _locations[p] + offset;
                        var x = location % Width;
                        var y = location / Width;
                        // ... and plot it on the console
                        Console.SetCursorPosition(x, y);
                        Console.Write(piece.Marker);
                    }
                }
                // Send the cursor to the bottom and wait for a key
                Console.SetCursorPosition(0, Height);
                Console.ReadKey();
            }
    
            // IEqualityComparer<Board> implemented on this class for convenience
    
            // Checks if two boards are identical
            public bool Equals(Board x, Board y)
            {
                return Enumerable.SequenceEqual(x._locations, y._locations);
            }
    
            // Gets a unique-ish hash code for the board
            // XORs the shifted piece locations into an int
            public int GetHashCode(Board b)
            {
                var hash = 0;
                var shift = 0;
                foreach (var i in b._locations)
                {
                    hash ^= (i << shift);
                    shift += 4;
                }
                return hash;
            }
        }
    
        // Representation of a piece, its visual representation, and the cells it occupies
        private class Piece
        {
            public char Marker { get; private set; }
            public int[] Offsets { get; private set; }
    
            public Piece(char marker, params int[] offsets)
            {
                Marker = marker;
                Offsets = offsets;
            }
        }
    }
Page 28 of 28 (277 items) «2425262728