65.9K
CodeProject 正在变化。 阅读更多。
Home

绑定点滴 - MVVM化字节/位数据

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2012年10月12日

CPOL

7分钟阅读

viewsIcon

18349

downloadIcon

372

基于字节/位数据实现MVVM的不同方法。

Sample Image

引言

在大多数常见场景中,构建基于MVVM的解决方案涉及以下步骤:

  1. 获取数据。
  2. 将数据塑造成可以以所需方式由UI元素显示的结构。
  3. 将数据放入(“可通知的”)属性中。
  4. 使用绑定机制将UI(视图)“粘合”到保存塑形数据(ViewModel)的属性。

步骤2、3被认为是ViewModel,因为它包含面向数据并塑造成与显示方式匹配的数据模型。步骤4——将UI元素与匹配的模型配对通常在View内部通过XAML的绑定标记扩展来完成。

当数据仍然与UI的表示不同,或者当UI更改需要表示为数据更改时,我们使用Value-Converters来弥合这个差距,并进行UI到数据以及反向的转换。

这种常见范式几乎可以服务于99%的案例。我将展示一个可能更好的不同方法的案例。

背景

考虑以下情况:

底层数据是一个由数百个字节组成的字节块,其中每个字节、字节簇、位、位簇都具有某种有意义的值,其类型(即字符、字符串、数字、布尔值、枚举值等)不同。更复杂的是,有些值实际上决定了其他字节/位的值和类型(例如,字节#m中位#3的值将决定字节#x的值是表示一个字符还是8个布尔值)。这些数据应该被解释为UI,允许用户查看/编辑并保存(作为字节块)。

解决方案

注意事项

为了理解以下解决方案,您应该熟悉两件事:

  1. 基本的位操作:网络上有很多关于它们的源代码和教程。
  2. 在解决方案中,我使用了一个名为BcpBinding的自定义标记扩展。它是一种特殊的绑定,允许将绑定表达式传递给其ValueConverter的ConverterParameter(以及其他一些很酷的功能...)。您可以在我的文章可绑定转换器参数中阅读更多相关信息。

使用常见范式

如简介部分所述,一旦我们有了数据,我们要做的第一件事就是将其“映射”到许多“可通知”属性中,每个属性都将具有其底层字节/位实际代表的实际类型。然后,我们将用其所依赖的字节/位的转换值填充这些属性中的每一个。之后,我们将不得不为不同属性依赖于相同字节/位源的情况构建一个精密的通知机制。我们还需要构建一个“反向转换”机制,该机制将从每个属性中提取值并将其转换回其字节/位值,以便在修改后可以保存该块。所有这些步骤都应该在View-Model内部实现。

最后,我们将构建UI(XAML),它将被映射并绑定到View-Model中的匹配属性。

尽管该解决方案可能被认为是合理的,但它将导致一个过大且复杂的View-Model,这将变得非常难以维护和调试。

替代方法

有几个线索可能会引导我们找到一个更好的解决方案,这些线索与当前问题的性质有关:

  1. 实际决定字节/位值性质的是它在块中的位置。
  2. 尽管每个字节/位实例具有不同的含义,但它所代表的值类型可以映射到有限的几种类型(字符、字符串、数字、枚举等),因此尽管每个字段的底层数据可能是不同的字节/位配置,但可以使用有限数量的参数化函数来实现转换。
  3. 由于数据是字节/位类型,转换和操作它将涉及使用位操作。实际上只有非常少的基本位操作(移位、AND、OR、XOR等)。
  4. 将值直接绑定到原始数据(而不是绑定到经过处理的数据层)将导致代码量大大减少,并允许我们省略上一节中描述的更改通知机制,因为由其绑定的UI元素对字节进行的更改将隐式触发绑定到相同字节的其他UI元素的更改。

解决方案本身

我们的ViewModel将不为每个单独的值设置一个属性,而是只拥有一个单一属性来以其最原始的状态保存整个字节。即,索引的字节集。视图(XAML)将把其每个元素直接绑定到它所代表的字节/位。数据从其源类型(字节/位)的转换将使用有限的一组参数化——双向-值转换器(VALUE_CONVERTERS)完成。这些值转换器将使用有限的一组位操作静态函数。

Using the Code

视图模型 (The View-Model)

如前所述,它只有一个属性,该属性以其最原始(未更改)的状态保存整个数据块。

//// 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"); }
}

警惕索引器绑定

绑定到索引结构是一种众所周知的技术,尽管其更改通知机制的信息有些模糊。根据我收集到的信息,当可通知索引器(ObservableCollection)的值发生变化时,索引器将引发一个INotifyPropertyChanged事件,并将字符串 - Item[] 作为属性标识符,这实际上是Binding类的一个常量 - Binding.IndxerName

每个绑定到索引器实际上都“监听”相同的属性名通知。这意味着对于索引器中任何成员的任何更改,绑定到同一索引器中其他成员的所有其他元素都将重新评估其绑定,如果绑定具有ValueConverter,它也将被重新激活(没有任何实际需要)。这种行为可能会对性能和意外结果产生重大影响。

为了解决这种不希望的行为,我将每个字节封装在一个INotifyPropertyChanged类(名为NotifiableByte)中,该类包含一个名为Value的属性来保存实际字节,并使用List(of)<NotifiableByte>。在视图中:绑定路径现在设置为'Bytes[#n].Value'(而不是'Bytes[#n]')。

* 如果有读者对此问题有更多了解,或/并知道更好的解决方法,我将不胜感激。

View

视图元素值将直接绑定到字节。每个绑定都将以双向模式使用“通用”参数化转换器参数,以便将字节/位值转换为UI元素显示的值。

在某些情况下,当将UI元素的值转换为其字节/位形式(ConvertBack)时,转换器的ConvertBack方法需要原始字节/位值。在这种情况下,我们将使用BcpBinding自定义标记扩展,因为它允许我们将原始字节作为转换器的转换器参数传递,以及为了从UI元素的值生成有效字节值所需的其他信息。

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

转换器

如前所述,字节转换操作可以简化为少数几个基本函数(带参数)。并且可以被其他转换函数(重新)使用。这使我们只剩下少数几个转换函数,它们甚至可以处理大型和复杂的字节块集。

 ...
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] };
    }
}
....

通过使用基本位操作的静态类,进一步实现了代码重用。

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));
    }

}

结论

总而言之,我们在这里实际做了什么:

  1. 我们将所有逻辑从ViewModel转移到一组基本、通用、参数化的ValueConverters。这些转换器使用基本位操作函数的一个子集。
  2. ViewModel现在以其最未修改的状态(包装字节的索引器)保存底层数据。
  3. View直接绑定到ViewModel索引器中的字节。而从字节/位的原始数据转换为其实际表示(反之亦然)则通过双向模式的ValueConverters完成。
  4. 当转换器“需要”额外信息来执行其Convert/ConvertBack方法时,我们使用BcpBinding自定义标记扩展,它允许我们将绑定作为其ConverterParameter参数传递。

关注点

这种方法最棒的一点是,无论您的底层数据(字节块)在体积和复杂性上如何增长,处理它的代码都将保持相对不变!

© . All rights reserved.