绑定点滴 - MVVM化字节/位数据
基于字节/位数据实现MVVM的不同方法。
引言
在大多数常见场景中,构建基于MVVM的解决方案涉及以下步骤:
- 获取数据。
- 将数据塑造成可以以所需方式由UI元素显示的结构。
- 将数据放入(“可通知的”)属性中。
- 使用绑定机制将UI(视图)“粘合”到保存塑形数据(ViewModel)的属性。
步骤2、3被认为是ViewModel,因为它包含面向数据并塑造成与显示方式匹配的数据模型。步骤4——将UI元素与匹配的模型配对通常在View内部通过XAML的绑定标记扩展来完成。
当数据仍然与UI的表示不同,或者当UI更改需要表示为数据更改时,我们使用Value-Converters来弥合这个差距,并进行UI到数据以及反向的转换。
这种常见范式几乎可以服务于99%的案例。我将展示一个可能更好的不同方法的案例。
背景
考虑以下情况:
底层数据是一个由数百个字节组成的字节块,其中每个字节、字节簇、位、位簇都具有某种有意义的值,其类型(即字符、字符串、数字、布尔值、枚举值等)不同。更复杂的是,有些值实际上决定了其他字节/位的值和类型(例如,字节#m中位#3的值将决定字节#x的值是表示一个字符还是8个布尔值)。这些数据应该被解释为UI,允许用户查看/编辑并保存(作为字节块)。
解决方案
注意事项
为了理解以下解决方案,您应该熟悉两件事:
- 基本的位操作:网络上有很多关于它们的源代码和教程。
- 在解决方案中,我使用了一个名为BcpBinding的自定义标记扩展。它是一种特殊的绑定,允许将绑定表达式传递给其ValueConverter的ConverterParameter(以及其他一些很酷的功能...)。您可以在我的文章可绑定转换器参数中阅读更多相关信息。
使用常见范式
如简介部分所述,一旦我们有了数据,我们要做的第一件事就是将其“映射”到许多“可通知”属性中,每个属性都将具有其底层字节/位实际代表的实际类型。然后,我们将用其所依赖的字节/位的转换值填充这些属性中的每一个。之后,我们将不得不为不同属性依赖于相同字节/位源的情况构建一个精密的通知机制。我们还需要构建一个“反向转换”机制,该机制将从每个属性中提取值并将其转换回其字节/位值,以便在修改后可以保存该块。所有这些步骤都应该在View-Model内部实现。
最后,我们将构建UI(XAML),它将被映射并绑定到View-Model中的匹配属性。
尽管该解决方案可能被认为是合理的,但它将导致一个过大且复杂的View-Model,这将变得非常难以维护和调试。
替代方法
有几个线索可能会引导我们找到一个更好的解决方案,这些线索与当前问题的性质有关:
- 实际决定字节/位值性质的是它在块中的位置。
- 尽管每个字节/位实例具有不同的含义,但它所代表的值类型可以映射到有限的几种类型(字符、字符串、数字、枚举等),因此尽管每个字段的底层数据可能是不同的字节/位配置,但可以使用有限数量的参数化函数来实现转换。
- 由于数据是字节/位类型,转换和操作它将涉及使用位操作。实际上只有非常少的基本位操作(移位、AND、OR、XOR等)。
- 将值直接绑定到原始数据(而不是绑定到经过处理的数据层)将导致代码量大大减少,并允许我们省略上一节中描述的更改通知机制,因为由其绑定的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));
}
}
结论
总而言之,我们在这里实际做了什么:
- 我们将所有逻辑从ViewModel转移到一组基本、通用、参数化的ValueConverters。这些转换器使用基本位操作函数的一个子集。
- ViewModel现在以其最未修改的状态(包装字节的索引器)保存底层数据。
- View直接绑定到ViewModel索引器中的字节。而从字节/位的原始数据转换为其实际表示(反之亦然)则通过双向模式的ValueConverters完成。
- 当转换器“需要”额外信息来执行其Convert/ConvertBack方法时,我们使用BcpBinding自定义标记扩展,它允许我们将绑定作为其
ConverterParameter
参数传递。
关注点
这种方法最棒的一点是,无论您的底层数据(字节块)在体积和复杂性上如何增长,处理它的代码都将保持相对不变!