11
\$\begingroup\$

This is a tool I needed for some other things (it's incomplete, but almost done) which is essentially just a text-editor that has two text-boxes: the left being the binary view / raw bytes, the right being the text when the bytes are decoded with the specified Encoding.

The only features it doesn't yet have that I need to add are Open, Save, and New for the most part. (Maybe a couple others that may be handy, but not necessary right now.)

It's entirely in WPF / XAML / C# using MVVM for everything.

This first bit is the MainWindow.xaml.cs, there's almost nothing to this part except setting up the DataContext:

namespace BinaryEditor
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = new MainWindowViewModel();
        }
    }
}

Next, we have the MainWindow.xaml which is obviously where all the XAML happens:

<Window x:Class="BinaryEditor.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:BinaryEditor"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525"
        d:DataContext="{d:DesignInstance local:MainWindowViewModel}">
    <Window.Resources>
        <local:ByteStringConverter x:Key="StringToByteConverter" />
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <TextBlock Grid.Column="0" Grid.Row="0" Text="Bytes per Row: " />
                <TextBox Grid.Column="1" Grid.Row="0" FontFamily="Consolas" Text="{Binding BytesPerLine, UpdateSourceTrigger=PropertyChanged}" />
            </Grid>
            <Grid Grid.Row="1">
                <TextBox FontFamily="Consolas"
                         VerticalScrollBarVisibility="Visible"
                         HorizontalScrollBarVisibility="Auto">
                    <TextBox.Text>
                        <MultiBinding Converter="{StaticResource StringToByteConverter}" UpdateSourceTrigger="PropertyChanged">
                            <Binding Path="BytesString" />
                            <Binding Path="BytesPerLine" />
                        </MultiBinding>
                    </TextBox.Text>
                </TextBox>
            </Grid>
        </Grid>
        <StackPanel Orientation="Horizontal" Grid.Row="3" Grid.Column="0">
            <TextBlock Grid.Row="2" Grid.Column="0" FontFamily="Consolas" Text="{Binding BytesString.Length}" />
            <TextBlock Grid.Row="2" Grid.Column="0" FontFamily="Consolas" Text=" characters, " />
            <TextBlock Grid.Row="2" Grid.Column="0" FontFamily="Consolas" Text="{Binding ByteCount}" />
            <TextBlock Grid.Row="2" Grid.Column="0" FontFamily="Consolas" Text=" bytes" />
        </StackPanel>
        <GridSplitter Grid.Column="1"
                      Grid.RowSpan="2"
                      HorizontalAlignment="Stretch"
                      VerticalAlignment="Stretch"
                      Width="5"
                      ShowsPreview="True"
                      ResizeDirection="Columns" />
        <Grid Grid.Column="2">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <TextBlock Grid.Column="0" Text="Encoding: " />
                <ComboBox Grid.Column="1" SelectedIndex="{Binding SelectedEncoding}" ItemsSource="{Binding Encodings}" />
                <TextBlock Grid.Column="0" Grid.Row="1" Text="Font: " />
                <ComboBox Grid.Column="1" Grid.Row="1" SelectedIndex="{Binding SelectedFont}" ItemsSource="{Binding Fonts}" />
            </Grid>
            <Grid Grid.Row="1">
                <TextBox Grid.Row="2"
                         Grid.Column="2"
                         FontFamily="{Binding SelectedFontString}"
                         AcceptsReturn="True"
                         AcceptsTab="True"
                         VerticalScrollBarVisibility="Visible"
                         Text="{Binding Text, UpdateSourceTrigger=PropertyChanged}" />
            </Grid>
        </Grid>
        <StackPanel Orientation="Horizontal" Grid.Row="3" Grid.Column="2">
            <TextBlock Grid.Row="2" Grid.Column="0" FontFamily="Consolas" Text="{Binding Text.Length}" />
            <TextBlock Grid.Row="2" Grid.Column="0" FontFamily="Consolas" Text=" glyphs" />
        </StackPanel>
    </Grid>
</Window>

There's really nothing complex here except maybe the MultiBinding that uses the converter, so it seems fitting to post the ByteStringConverter.cs next:

namespace BinaryEditor
{
    public class ByteStringConverter : IMultiValueConverter
    {
        public object Convert(object[] value, Type targetType, object parameter, CultureInfo culture)
        {
            var valueAsString = (value[0] as string).ToUpper();
            var bytesPerRow = (int)value[1];

            if (bytesPerRow == 0)
            {
                bytesPerRow = 8;
            }

            var sb = new StringBuilder((int)(valueAsString.Length * 1.5));

            for (int i = 0; i < Math.Ceiling(valueAsString.Length / 2.0); i++)
            {
                if (sb.Length > 0)
                {
                    sb.Append(' ');

                    if (i % bytesPerRow == 0)
                    {
                        sb.Append(Environment.NewLine);
                    }
                }

                if (i * 2 + 1 < valueAsString.Length)
                {
                    sb.Append(valueAsString[i * 2]);
                    sb.Append(valueAsString[i * 2 + 1]);
                }
                else
                {
                    sb.Append('_');
                    sb.Append(valueAsString[i * 2]);
                }
            }

            return sb.ToString();
        }

        public object[] ConvertBack(object value, Type[] targetType, object parameter, CultureInfo culture)
        {
            var valueAsString = value as string;
            return new object[] { valueAsString.Replace(" ", "").Replace("_", "").Replace(Environment.NewLine, "") };
        }
    }
}

The ConvertBack method doesn't return a value for the BytesPerRow which is entirely acceptable in an IMultiValueConverter. (It could, for the most part, I would just have to count how many bytes are in the first row, but I'm too lazy to do that and it would then rewrite the binding for BytesPerRow which I don't want.)

Finally, the one bit I left out is the MainWindowViewModel.cs, which is below:

namespace BinaryEditor
{
    public class MainWindowViewModel : INotifyPropertyChanged
    {
        private string _bytesString = "";
        private int _bytesPerLine = 8;
        private int _selectedEncoding = 1;
        private int _selectedFont = 0;
        private const string _validCharacters = "0123456789abcdefABCDEF";

        public string BytesString
        {
            get
            {
                return _bytesString;
            }
            set
            {
                foreach (var c in value)
                {
                    if (!_validCharacters.Contains(c))
                    {
                        return;
                    }
                }

                _bytesString = value;

                OnPropertyChanged(new PropertyChangedEventArgs(nameof(BytesString)));
                OnPropertyChanged(new PropertyChangedEventArgs(nameof(ByteCount)));
                OnPropertyChanged(new PropertyChangedEventArgs(nameof(Text)));
                OnPropertyChanged(new PropertyChangedEventArgs(nameof(GlyphCount)));
            }
        }

        public int ByteCount
        {
            get
            {
                return _bytesString.Length / 2;
            }
        }

        public string Text
        {
            get
            {
                var bytes = Enumerable.Range(0, (int)Math.Ceiling(BytesString.Length / 2.0)).Select(i => Convert.ToByte(BytesString.Length == 0 ? null : i * 2 + 1 < BytesString.Length ? BytesString.Substring(i * 2, 2) : "0" + BytesString.Last(), 16)).ToArray();
                switch (SelectedEncoding)
                {
                    case 1:
                        return string.Join("", Encoding.UTF8.GetString(bytes));
                    case 2:
                        return string.Join("", Encoding.Unicode.GetString(bytes));
                    case 3:
                        return string.Join("", Encoding.UTF32.GetString(bytes));
                    default:
                        return string.Join("", Encoding.ASCII.GetString(bytes));
                }
            }
            set
            {
                var bytes = new byte[0];
                switch (SelectedEncoding)
                {
                    case 1:
                        bytes =  Encoding.UTF8.GetBytes(value);
                        break;
                    case 2:
                        bytes = Encoding.Unicode.GetBytes(value);
                        break;
                    case 3:
                        bytes = Encoding.UTF32.GetBytes(value);
                        break;
                    default:
                        bytes = Encoding.ASCII.GetBytes(value);
                        break;
                }

                _bytesString = string.Join("", bytes.Select(v => v.ToString("X2")));

                OnPropertyChanged(new PropertyChangedEventArgs(nameof(BytesString)));
                OnPropertyChanged(new PropertyChangedEventArgs(nameof(ByteCount)));
                OnPropertyChanged(new PropertyChangedEventArgs(nameof(Text)));
                OnPropertyChanged(new PropertyChangedEventArgs(nameof(GlyphCount)));
            }
        }

        public int GlyphCount
        {
            get
            {
                return Text.ToCharArray().Length;
            }
        }

        public int BytesPerLine
        {
            get
            {
                return _bytesPerLine;
            }
            set
            {
                _bytesPerLine = value;
                OnPropertyChanged(new PropertyChangedEventArgs(nameof(BytesPerLine)));
            }
        }

        public int SelectedEncoding
        {
            get
            {
                return _selectedEncoding;
            }
            set
            {
                var bytes = BytesString;
                _selectedEncoding = value;
                OnPropertyChanged(new PropertyChangedEventArgs(nameof(SelectedEncoding)));
                BytesString = bytes;
            }
        }

        public int SelectedFont
        {
            get
            {
                return _selectedFont;
            }
            set
            {
                _selectedFont = value;
                OnPropertyChanged(new PropertyChangedEventArgs(nameof(SelectedFont)));
                OnPropertyChanged(new PropertyChangedEventArgs(nameof(SelectedFontString)));
            }
        }

        public string SelectedFontString => Fonts[SelectedFont];

        public List<string> Fonts
        {
            get
            {
                return new List<string>
                {
                    "Consolas",
                    "Arial",
                    "Times New Roman"
                };
            }
        }

        public List<string> Encodings
        {
            get
            {
                return new List<string>
                {
                    "ASCII",
                    "UTF8",
                    "UTF16",
                    "UTF32"
                };
            }
        }

        public void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            var handler = PropertyChanged;
            handler?.Invoke(this, e);
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }
}

Feel free to review everything. There's literally nothing else required to make this work, feel free to create a new solution in Visual Studio (mine is named BinaryEditor) and drop all this in to see it work. It's actually pretty neat.

And as requested, an image of the programme in action (I've added the File menu and fixed margins since then, but the main gist of it is there):

Demo GIF

\$\endgroup\$
  • 2
    \$\begingroup\$ Could you include a screenshot in the question. I'm just curious :) \$\endgroup\$ – jacwah Nov 5 '16 at 19:07
  • \$\begingroup\$ @jacwah I've made some changes since then, but yeah, I'll post a screenshot of the current version. :) \$\endgroup\$ – 410_Gone Nov 5 '16 at 19:21
4
\$\begingroup\$

I've spotted a few minor issues in your code.


You might consider encapsulating these formulas:

Math.Ceiling(valueAsString.Length / 2.0)

and

valueAsString.Length * 1.5

and

i * 2 + 1

because they appear in multiple places namely the ByteStringConverter and the MainWindowViewModel - both use them.


SelectedFontString 

I think this property should be called SelectedFontFamily because you bind it to the FontFamily property in XAML.


            switch (SelectedEncoding)
            {
                case 1:
                    return string.Join("", Encoding.UTF8.GetString(bytes));
                case 2:
                    return string.Join("", Encoding.Unicode.GetString(bytes));
                case 3:
                    return string.Join("", Encoding.UTF32.GetString(bytes));
                default:
                    return string.Join("", Encoding.ASCII.GetString(bytes));
            }

Those cases aren't very pretty. An enum would be optimal.


            var bytes = BytesString;
            _selectedEncoding = value;
            OnPropertyChanged(new PropertyChangedEventArgs(nameof(SelectedEncoding)));
            BytesString = bytes;

This looks suspicious. Shouldn't there be another call to OnPropertyChanged but for the BytesString?


And one more thing...

    public void OnPropertyChanged(PropertyChangedEventArgs e)
    {
        var handler = PropertyChanged;
        handler?.Invoke(this, e);
    }

If you already have such method then let it work with a string and create the EventArgs inside this method. The calls to OnPropertyChagned will be much shorter like OnPropertyChanged(nameof(BytesString));

\$\endgroup\$
  • 1
    \$\begingroup\$ Updated all of them in my source (except the formulas so far), as far as "This looks suspicious. Shouldn't there be another call to OnPropertyChanged but for the BytesString?": no, when I do BytesString = bytes; at the bottom of that block, it calls the set on BytesString which calls OnPropertyChanged for itself automagically. \$\endgroup\$ – 410_Gone Nov 5 '16 at 19:43
2
\$\begingroup\$

The !_validCharacters.Contains(c) condition is not very efficient, because it does a linear search in _validCharacters = "0123456789abcdefABCDEF". You could use a set or range checks instead. But if you don't experience slowness in the UI then it might not be worth the effort and you can just leave as it is, simple.


This line is effectively unreadable:

var bytes = Enumerable.Range(0, (int)Math.Ceiling(BytesString.Length / 2.0)).Select(i => Convert.ToByte(BytesString.Length == 0 ? null : i * 2 + 1 < BytesString.Length ? BytesString.Substring(i * 2, 2) : "0" + BytesString.Last(), 16)).ToArray();

Definitely break it down to multiple lines so that it becomes readable without having to scroll horizontally several screens.


Instead of Text.ToCharArray().Length, why not Text.Length() ?

\$\endgroup\$
  • \$\begingroup\$ Yeah that Enumerable.Range line might be abuse. \$\endgroup\$ – 410_Gone Jan 19 '17 at 4:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.