Click here to Skip to main content
15,867,756 members
Articles / Desktop Programming / WPF

A bit of binding - MVVM'ing bytes/bits based data

Rate me:
Please Sign up or sign in to vote.
5.00/5 (3 votes)
12 Oct 2012CPOL7 min read 17.1K   328   10   2
Different approaches for implementing MVVM over byte/bit based data.

Sample Image

Introduction

Under most common scenarios, constructing an MVVM based solution involves the following steps:

  1. Get hold of the data.
  2. Mold data into structures that could be displayed by the UI-Elements in a desirable manner.
  3. Place the data inside ('Notifiable') properties.
  4. 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:

  1. Fundamental bit-operations: There are a lot of source code and tutorials about them in the web.
  2. 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:

  1. The thing that actually determines the nature of the bytes/bits value is its position in the block.
  2. 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.
  3. 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.).
  4. 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:

XML
//// 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.

XML
...
<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.

C#
 ...
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.

C#
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:

  1. We shifted all logic from the ViewModel into a set of fundamental, generic, parameterized ValueConverters. These use a subset of fundamental bit-operation functions.
  2. ViewModel now holds the underlying data in its most unmodified state (indexer of wrapped bytes).
  3. 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.
  4. 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!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) self employed
Israel Israel
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
PraiseThank you! Pin
kamil.adamczuk31-May-19 13:57
kamil.adamczuk31-May-19 13:57 
GeneralRe: Thank you! Pin
ntg12326-Aug-19 19:58
ntg12326-Aug-19 19:58 
Hi kamil,

Great to hear.

Thank you

g

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.