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):