Introduction
Under most common scenarios, constructing an MVVM based solution involves the following steps:
- Get hold of the data.
- Mold data into structures that could be displayed by the UI-Elements in a desirable manner.
- Place the data inside ('Notifiable') properties.
- Use the binding mechanism to 'glue' UI (View) to the properties that hold the molded-data (ViewModel).
Steps 2, 3 are considered as the ViewModel as it consists of models of data oriented
and shaped to match the way it will displayed.
Step 4 - pairing UI-elements to the matching Models is usually done inside the View via XAML's markup-extensions for binding.
When data is still different than the UI's representation of it, or when UI changes need to be expressed as changes of data, we use Value-Converters to
bridge that gap, and translate UI-to-Data and vice verse.
This common paradigm can serve us for almost 99 % of cases. I'll show one case where a different approach might be better.
Background
Consider the following case:
The underlying data is a bytes block which consists of hundreds of bytes, where each
byte, cluster of bytes, bit, cluster of bits, have some meaningful value of
some type (i.e., Char, String, Number, Boolean, Enum value, and so on...). To make things even more interesting some of the values actually determine other
bytes/bits values and types (e.g., the value of bit#3 in byte#m will determine if byte#x value represents a char or 8
boolean values).
This data should be interpreted into a UI that will allow a user to view/edit
and save (as bytes block) it.
The Solution
A Note
There are two things you should be familiar with in order to comprehend the following solution:
- Fundamental bit-operations: There are a lot of source code and tutorials about them in the web.
- In the solution, I use a custom markup extension called BcpBinding. It is a special kind of Binding that allows passing Binding-Expressions
to its ValueConverter's ConverterParameter (and some other cool stuff...). You can read more about it in my article
Bindable Converter Parameter.
Using the Common Paradigm
As described in the Introduction section, the first thing we'll do (once we have
the data) is 'map' it into numerous 'Notifiable' properties on which each
property will have the actual type its underlying byte/bit actually
represents.
Then, we would fill each of these properties with a converted value of the
byte/s bit/s it relies upon.
After that we would have to build an elaborate notification mechanism for those
cases that different properties rely on the same byte/bits source.
We would also need to build a 'convert-back' mechanism that will extract values
from each property and convert it back to its byte/bit value, so the block
could be saved after modified.
All of theses steps should be implemented inside the View-Model.
Finally, we would construct the UI (XAML) that will be mapped and bound to the
matching properties in the View-Model.
Although the solution might be considered reasonable, it will result in having an
oversized and complex View-Model which would become very hard to maintain and
debug.
The Alternative Approach
There are several clues that might lead us to a better solution, which are
related to the nature of the problem at hand:
- The thing that actually determines the nature of the bytes/bits value is
its
position in the block.
- Although every byte/bit instance has different meaning, the value type it
represents can be mapped into limited kinds of types (char, string,
number, enum,
etc.), so although the underlying data for each field might be a different set
of byte/bits configuration, conversion could be implemented using a limited
number of parameterized-functions.
- As data is of byte/bit types, converting and manipulating it would involve
using bit-operations. There are actually very few fundamental bit-operations (shift, AND, OR, XOR etc.).
- Having the values bound directly to raw data (vs. binding
to a digested layer of this data) will result in much less code, and will allow
us to omit the change-notification mechanism described in the previous section,
as changes to a byte made by its bound UI-Element will implicitly trigger
changes in other UI-elements bound to the same byte.
The Solution Itself
Instead of having a property to match every individual value, our View-Model will
have only a single property that will hold the
entire bytes in its most raw state. I.e., indexed set of bytes.
The View (XAML) will bind each of its elements directly
to the byte/s, bit/s it represents.
Conversion of the data from its source type (byte/bit) will be done using a
limited set of parameterized --TWO_WAY-VALUE_CONVERTERS.
Those Value-Converters will use a limited set of bit-operation static
functions.
Using the Code
The View-Model
As mentioned earlier it has only a single property that holds the entire block of
data in its most raw (unchanged) state:
//// 1st method :not valid as it doesn't notify single byte value changedd
//private byte[] _Bytes = new byte[] { 0x0, 0xff, 0x1, 0x2, 0x3 };
//public byte[] Bytes
//{
// get { return _Bytes; }
// set { SetProperty(ref _Bytes, value, "Bytes"); }
//}
//// 2st method : not valid as it raises 'Item[]' property
//// changed event, for every single member of the collection change.
//private ObservableCollection<byte> _Bytes =
// new ObservableCollection<byte>( new byte[]{ 0x0, 0x55, 0xFF, 0x1, 0x3} );
//public ObservableCollection<byte> Bytes
//{
// get { return _Bytes; }
// set { SetProperty(ref _Bytes, value, "Bytes"); }
//}
//// 3rd method : the use of List<> allow indexed-binding. Property-Changed-Event
//// is raised for specific Value(not the entire collection !!!)
private List<NotifiableByte> _Bytes =new List<NotifiableByte>(
(new byte[] { 0x0, 0x55, 0xFF, 0x1, 0x3 }).Select(b=>new NotifiableByte(){Value=b}));
public List<NotifiableByte> Bytes
{
get { return _Bytes; }
set { SetProperty(ref _Bytes, value, "Bytes"); }
}
Beware of Indexer Bindings
Binding to indexed structures is a well known technique, although information
about its change-notification-mechanism is somehow oblique.
From what I have gathered, when a Notifiable Indexer (ObservableCollection
) value changes, the indexer will raise
an INotifyPropertyChanged
event with this
string - Item[]
as the Property-Identifier, which is actually
a constant of the Binding
class - Binding.IndxerName
.
Every binding to the indexer actually 'listens' to the same
property-name notification. This means that for any change of
any member of the indexer, all other elements bound to other members in the same indexer will have their
Binding
re-evaluated, and if a Binding has a ValueConverter
, it will
also be
re-activated (without any actual need).
This behavior has a potential to have significant performance and
unexpected-result implications.
To solve this undesired behavior, I've wrapped every byte inside an INotifyPropertyChanged
class (called NotifiableByte
) with a property called Value
to hold the actual
byte, and use a List(of)<NotifiableByte>
.
In the View: bindings paths are now set to 'Bytes[#n].Value
' (instead of 'Bytes[#n]
').
* If any of the readers know more about this issue, or/and knows a better
workaround for it, I'll be grateful to hear.
The View
View element values will be bound directly to bytes. Each Binding will use
a 'generic', parameterized Converter parameter in a Two-Way mode in order to
convert byte/bit value into the UI-element's displayed value.
In some cases, when converting a UI-element's value into its byte/s form
(ConvertBack
), the original byte/s value is needed by the Converter's
ConvertBack
method. In such cases, we will use the BcpBinding
Custom-Markup-Extension, as it allows us to pass the original byte as a
converter's converter-parameter, along with other information we'll need in
order to produce a valid byte value out of the UI-element's value.
...
<StackPanel Orientation="Horizontal" >;
<TextBlock Text="Byte #0 Direct Binding :"/>
<TextBox >
<TextBox.Text >
<Binding Path="Bytes[0].Value" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged" >
<Binding.ValidationRules >
<local:ByteTextBoxValidationRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
</StackPanel>
<StackPanel Orientation="Horizontal" >
<TextBlock Text="Byte #1 Bits Editing : "/>
<StackPanel>
<CheckBox Content=": Bit #0" FlowDirection="RightToLeft">
<CheckBox.IsChecked >
<local:BcpBinding Path="Bytes[1].Value"
Converter="{StaticResource ByteBit2Bool}"
ConverterParameters="0,Binding Path=Bytes[1].Value" Mode="TwoWay" />
</CheckBox.IsChecked>
</CheckBox>
...
The Converters
As mentioned earlier, byte converting operations can be narrowed down to just a
few fundamental functions (with parameters). And could, also, be (re)used by other
converting functions.
This leaves us with just a handful of conversion-functions that could handle
even large and complex bytes-blocks sets.
...
class ByteBit2Bool : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
object[] parameters = (object[])parameter;
int b = (byte)value;
int bitNumber = int.Parse(parameters[0].ToString());
bool ret = (b & (1 << bitNumber)) > 0;
return ret;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
object[] parameters = (object[])parameter;
int bitNumber = int.Parse(parameters[0].ToString());
byte byteOrigVal=(byte)parameters[1];
byte ret;
if ((bool)value)
{
ret = (byte)(byteOrigVal | (byte)Math.Pow(2, bitNumber));
}
else
{
ret =BitOperations.ZeroBit(byteOrigVal, bitNumber);
}
return ret;
}
}
class TwoBytes2Value : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values == null || values.Any(v => v == null || v == DependencyProperty.UnsetValue))
{
return "";
}
byte high = (byte)values[0];
byte low = (byte)values[1];
UInt16 HL = (UInt16)((high << 8) + low);
return HL.ToString();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
UInt16 u16val = UInt16.Parse(value.ToString());
byte[] hl = BitConverter.GetBytes(u16val);
return new object[2] { hl[1], hl[0] };
}
}
....
Further code reuse is achieved using a static class of fundamental bit operations.
internal static class BitOperations
{
internal static byte ExtractBitsValue(byte b, int startbit, int numofbits)
{
byte ls = (byte)(b << (8 - (startbit+numofbits)));
byte rs = (byte)(ls >> (8 - numofbits));
return rs;
}
internal static byte SetBitsValue(byte b, int startbit, int numofbits, byte newvalbyte)
{
byte bCleaned = ZeroBits(b, startbit, numofbits);
byte bUpdated = (byte)(bCleaned | newvalbyte);
return bUpdated;
}
internal static byte ZeroBits(byte b, int startbit, int numofbits)
{
for (int i = startbit; i < (startbit + numofbits); i++)
{
b = BitOperations.ZeroBit(b, i);
}
return b;
}
internal static byte ZeroBit(byte value, int position)
{
return (byte)(value & ~(1 << position));
}
}
In Conclusion
To summarize, what we
have actually done here:
- We shifted all logic from the ViewModel into a set of fundamental, generic,
parameterized ValueConverters. These use a subset of fundamental
bit-operation functions.
- ViewModel now holds the underlying data in its most unmodified state (indexer of
wrapped bytes).
- View binds directly to bytes in the ViewModel's indexer. While moving from
Bytes/Bits raw data into its actual representation (and vice verse) is done via
ValueConverters in a two-way mode.
- When Converter 'needs' additional information in order to perform its
Convert/ConvertBack methods, we use the BcpBinding custom-markup-extension, which
allows us to pass Bindings as its
ConverterParameter
parameter.
Points of Interest
One of the nicest thing about this approach is, no matter how your underlying data (bytes block) will grow in volume and complexity, the code for handling it
will stay relatively, the same!