前文再续,上一章提出了问题,本章提出了三种解决方案:
解决方案一:手动进行异步转换,核心思想:将binding做的事情放入CodeBehind
FilterItemControl.XAML:
FilterItemControl.cs
/// /// 设置数据源/// /// public async void SetSource(Filter filter){if (filter != null){_filter = filter;// 使用WriteableBitmap有一个不好的点:必须要知道图片的大小WriteableBitmap result = new WriteableBitmap(768, 1280);var wbData = await MyFilterSDK.ProcessFilterAsync(filter);if(wbData != null){using (var bmpStream = result.PixelBuffer.AsStream()){bmpStream.Seek(0, SeekOrigin.Begin);bmpStream.Write(wbData, 0, (int)bmpStream.Length);}FilterImage.Source = result;}FilterName.Text = filter.FilterName;}}
为其设置数据源, FilterItemsControl.cs
/// /// 数据源发生变化/// /// 滤镜列表private void OnItemsSourceChanged(List filters){if(filters != null){Container.Children.Clear();foreach(var filter in filters){FilterItemControl itemcontrol = new FilterItemControl();itemcontrol.Width = ITEMWIDTH;itemcontrol.Height = ITEMHEIGHT;// 将binding中做的事情放到代码中!itemcontrol.SetSource(filter);
itemcontrol.ItemSelected += Itemcontrol_ItemSelected;itemcontrol.ItemDoubleClicked += Itemcontrol_ItemDoubleClicked;Container.Children.Add(itemcontrol);}}}
优点:方便简单
缺点:XAML必须写死,没有扩展性,如果同样的数据变换一种显示方式,需要重写一个控件。
解决方案二:使用异步属性,核心思想,使用Binding和异步加载
FilterItemControl.xaml
FilterItemControl.cs不需要额外的东西,但是Model层的数据源需要增加一个异步属性用于被View层绑定
Filter.cs
private AsyncProperty _wbAsyncProperty; // 异步属性public AsyncProperty WBAsyncProperty{get{return _wbAsyncProperty;}set{SetProperty(ref _wbAsyncProperty, value);}}
// 初始化public Filter(){WBAsyncProperty = new AsyncProperty(async () =>{var result = await MyFilterSDK.ProcessFilterAsync(this);return result;});}
由于返回值是byte[]类型,所以我们在binding时,必须要进行一次转换,将其转换为WriteableBitmap:BytesToImageConverter.cs
public class BytesToImageConverter : IValueConverter{public object Convert(object value, Type targetType, object parameter, string language){// 使用WriteableBitmap有一个不好的点:必须要知道图片的大小WriteableBitmap result = new WriteableBitmap(768, 1280);var filterData = value as byte[];if (filterData != null){#region WriteableBitmap方案using (var bmpStream = result.PixelBuffer.AsStream()){bmpStream.Seek(0, SeekOrigin.Begin);bmpStream.Write(filterData, 0, (int)bmpStream.Length);return result;}#endregion}elsereturn null;}public object ConvertBack(object value, Type targetType, object parameter, string language){throw new NotImplementedException();}}
关于如何实现AsyncProperty和其工作原理在这里不做深究,在这里总结一下这个方案的优缺点:
优点:使用Binding,UI上不会卡顿,图片获取完之后会显示在UI上
缺点: 1. 控件重用性不高
2. SDK必须与UI无关,这也是为什么返回byte[],而不是直接返回WrieableBitmap的原因,与AsyncProperty的实现技术有关
3. 因为原因2,必须实现转换器
解决方案三:使用DataTemplate,核心思想:将DataTemplate转换放到CodeBehind
FilterItemControl.XAML需要改变,这里只需要一个ContentPresenter接收内容
FilterItemControl.cs需要增加一个ContentTemplate,用于获取应用在这个控件上的模板,并且根据模板把UI显示出来:
public DataTemplate ContentDataTemplate{get { return (DataTemplate)GetValue(ContentDataTemplateProperty); }set { SetValue(ContentDataTemplateProperty, value); }}public static readonly DependencyProperty ContentDataTemplateProperty =DependencyProperty.Register("ContentDataTemplate", typeof(DataTemplate), typeof(FilterItemControl3), new PropertyMetadata(null,OnContentDataTemplateChanged));private static void OnContentDataTemplateChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args){FilterItemControl3 owner = sender as FilterItemControl3;owner.OnContentDataTemplateChanged(args.NewValue as DataTemplate);}private async void OnContentDataTemplateChanged(DataTemplate newDataTemplate){UIElement rootElement = newDataTemplate.LoadContent() as UIElement;if(rootElement != null){Image img = VisualTreeExtensions.FindFirstElementInVisualTree(rootElement);if (img != null){#region 使用SDK 处理WriteableBitmap result = new WriteableBitmap(768, 1280);var wbData = await MyFilterSDK.ProcessFilterAsync(this.DataContext as Filter);if (wbData != null){using (var bmpStream = result.PixelBuffer.AsStream()){bmpStream.Seek(0, SeekOrigin.Begin);bmpStream.Write(wbData, 0, (int)bmpStream.Length);}img.Source = result;}#endregion// 改变了图片之后,需要将其加入到可视化中以显示,如果不加这一步你可以想象会出现什么情况Presenter.Content = rootElement;}}}
同样的,需要修改FilterItemsControl.cs,增加一个ItemDataTemplate传递给FilterItemControl:
/// /// 子项的模板/// public DataTemplate ItemDataTemplate{get { return (DataTemplate)GetValue(ItemDataTemplateProperty); }set { SetValue(ItemDataTemplateProperty, value); }}public static readonly DependencyProperty ItemDataTemplateProperty =DependencyProperty.Register("ItemDataTemplate", typeof(DataTemplate), typeof(FilterItemsControl3), new PropertyMetadata(0));/// /// 数据源发生变化/// /// 滤镜列表private void OnItemsSourceChanged(List filters){if (filters != null){Container.Children.Clear();foreach (var filter in filters){FilterItemControl3 itemcontrol = new FilterItemControl3();//itemcontrol.Width = ITEMWIDTH; // 不要了,在DataTemplate中指定//itemcontrol.Height = ITEMHEIGHT;//1. 设置DataContextitemcontrol.DataContext = filter;//2. 设置模板itemcontrol.ContentDataTemplate = ItemDataTemplate;itemcontrol.ItemSelected += Itemcontrol_ItemSelected;itemcontrol.ItemDoubleClicked += Itemcontrol_ItemDoubleClicked;Container.Children.Add(itemcontrol);}}}
那么我们只需要在使用这个控件的地方编写一个ItemDataTemplate就可以了:
第三种方案是我想表达的,但是我们看出来,它也并不是最优的,需要在代码中取出DataTemplate中的可视元素,然后将SDK处理过的图片放到目标Image控件的Source中去,但是他的优点也是有的: 1. UI的可扩展性
2. 与异步无关的属性可以通过Binding展示
可以说,方案三模拟了DataTemplate如何应用在一个控件上的,这也是我想从这个例子中总结的东西:
1. DataTemplate的作用
2. 控件在应用了DataTemplate之后发生了什么?
3. 通过DataTemplate.LoadContent(), 获取控件,并且修改控件,如果不使用Presenter.Content = rootElement, 为什么没有反应?
总结:
1. 首先DataTemplate的MSDN的解释非常清楚,就是将“数据"转换为可见的元素,这也是为什么我们选择DataTemplate来展示Filter的原因。
2. 控件在应用了DataTemplate之后会发生什么?因为微软的封闭,我们看不到,但是可以猜到,它的实现类似于我们方案三的实现:取得DataTemplate中的元素,并且将其加载到可视化树中显示。我们在XAML中写的DataTemplate类似于一个类的声明,当某个控件需要这个DataTemplate时,会new 一个实例,然后目标控件,并且替换它之前的可视化树。
3. 第三个问题的答案基于第二个问题:通过DataTemplate.LoadContent()获得的UIElement每次都是不一样的,就是说调用该方法就类似与调用 new DataTemplate(),一样,只是一次实例化,此时的元素并没有加载到可视化树中(可以通过GetHashCode()对比),所以,无论做什么修改,你都看不出结果。所以必须要有Presenter.Content = rootElement这关键的一步。
Demo已经写好,VS2015工程,WU框架,PC运行。
MyFilterDemo.rar