都说学以致用,怎么我入了 Xamarin 这个坑,就用不上呢?
闲了几个月,就憋了这个东西出来。
Android 截图:
UWP 截图:
从截图来看, UWP 的界面已经处理的和 Android 的高度相似了. 这可是我花了无数时 精雕细琢 的 (虽然还是丑).
看完图,上几个包,仅供预览……还有些BUG,所以……
Android 4.4 以上:
ARM : http://pan.baidu.com/s/1eQT6MJo
X86 (VS Android Emulator 可用): http://pan.baidu.com/s/1sjZqe7b
UWP (仅限手机版) : http://pan.baidu.com/s/1o6RUATk
https://github.com/gruan01/Xamarin.Forms.Lagou
拉勾官方 APP 使用的数据接口只有 ProtoBuf 格式,也就是说,除非你有它的实体类,否则要想解开这个 ProtoBuf 数据,那基本上是不可能的。
幸好,拉勾有手机版网站, 部分数据是 JSON 格式, 但部份数据还是要解析HTML才能得到。
因为使用的是手机版网站的数据,所以,用户体验不要指望和官方APP相比。
1, Xamarin.Forms
Xamarin.Forms 2.0 开始支持 UWP , 所以选用了2.0.1.6492-pre1 ,但是当前对 UWP 的支持仅仅是 “预览”, 这个版本上还充斥着大量的 BUG,比如 (以下仅指 UWP 手机版,桌面版没试):
A: 页面上如果有 ToolbarItems , 程序会直接Crash。
B:Image 在 Grid 中有很奇葩的表现。
C: Grid 对齐问题。
目前这几个BUG的影响比较大,非常期待下次更新能解决这些问题。但是 Xamarin 团队最近好像并不着急,都一个月多了, 还没有放出下一个版本。
2,Caliburn.Micro
当前使用的是3.0 beta-2, 基本上很稳定。遇到的问题只有一个,在 UWP 下, BindableCollection.AddRange 导致 ListView 数据发生重置(Reset , 会直接从第一条开始重新显示),这个问题在 WP8 SL 上也有。
不知道是 CM 的问题还是 微软的问题。
3, XamlSpy
XamlSpy 目前提供的测试版,可以很方便的探测到呈现出来的结构,在UWP下帮了我不少忙。Android 下虽然也可以用,但是作用不大。
之前是把 ViewModel 列表做为 ItemsSource 绑定到 TabbedPage 上,然后通过 CM 的 ViewLocator 功能,查找出 ViewModel 对应的视图,然后呈现。但是这样做有缺陷。
现在换一种全新的方法:Attached Property (BindableProperty.CreateAttached), 通过 Attached Property 的 PropertyChanged 事件对 TabbedPage 的 Children 直接进行修改:
1 public class TabbedPageVMLocatorBinder { 2 3 public static readonly BindableProperty VMsProperty = 4 BindableProperty.CreateAttached<TabbedPageVMLocatorBinder, INotifyCollectionChanged>( 5 o => GetVMsProperty(o), 6 null, 7 propertyChanged: VMSChanged); 8 9 10 public static INotifyCollectionChanged GetVMsProperty(BindableObject obj) { 11 if (null == (TabbedPage)obj) 12 throw new Exception("TabbedPageVMLocatorBinder only used for TabbedPage"); 13 14 return (INotifyCollectionChanged)obj.GetValue(VMsProperty); 15 } 16 17 private static void VMSChanged(BindableObject bindable, INotifyCollectionChanged oldValue, INotifyCollectionChanged newValue) { 18 var tab = (TabbedPage)bindable; 19 var tmp = new Tmp(tab, newValue); 20 } 21 22 private class Tmp : IDisposable { 23 24 private TabbedPage Tab = null; 25 26 public Tmp(TabbedPage tab, INotifyCollectionChanged vms) { 27 vms.CollectionChanged += Vms_CollectionChanged; 28 this.Tab = tab; 29 Vms_CollectionChanged(vms, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); 30 } 31 32 private void Vms_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { 33 var collection = (IList)sender; 34 e.Apply( 35 //insert 36 (o, i, b) => { 37 Insert(o, i, b); 38 }, 39 //remove 40 (o, i) => { 41 Remove(o, i); 42 }, 43 //reset 44 () => { 45 Reset(collection); 46 }); 47 } 48 49 private void Insert(object vm, int idx, bool b) { 50 var page = LocatePage(vm); 51 Tab.Children.Insert(idx, page); 52 } 53 54 private void Remove(object vm, int idx) { 55 Tab.Children.RemoveAt(idx); 56 } 57 58 private void Reset(IList items) { 59 Tab.Children.Clear(); 60 foreach (var o in items) { 61 Tab.Children.Add(LocatePage(o)); 62 } 63 } 64 65 private static Page LocatePage(object o) { 66 if (o == null) 67 throw new ArgumentNullException("o"); 68 69 var vmView = ViewLocator.LocateForModel(o, null, null); 70 if (vmView == null) 71 throw new Exception("没有找到视图"); 72 ViewModelBinder.Bind(o, vmView, null); 73 74 var activator = o as IActivate; 75 if (activator != null) 76 activator.Activate(); 77 78 var page = (Page)vmView; 79 if (page != null) { 80 page.Title = ((Screen)o)?.DisplayName; 81 return page; 82 } else 83 return null; 84 } 85 86 public void Dispose() { 87 88 } 89 } 90 }
1 <?xml version="1.0" encoding="utf-8" ?> 2 <TabbedPage xmlns="http://xamarin.com/schemas/2014/forms" 3 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 4 x:Class="Lagou.Views.TabView" 5 xmlns:local="clr-namespace:Lagou;assembly=Lagou" 6 local:TabbedPageVMLocatorBinder.VMs="{Binding Datas}" 7 BackgroundColor="#666666" 8 > 9 10 </TabbedPage>
说到 CreateAttached 必须指出一个奇葩, 使用的时候,要记住绕过它:
(错误)
1 public static readonly BindableProperty FontSizeProperty = 2 BindableProperty.CreateAttached<AttachedFontIcon, double>( 3 (double)o.GetValue(FontSizeProperty), 4 12 5 );
如果写成这样,在获取这个 Property 的值的时候, 返回的将是一个 string 类型的数据,而非 double 类型。
除非你这样写:
(正确)
1 public static readonly BindableProperty FontSizeProperty = 2 BindableProperty.CreateAttached<AttachedFontIcon, double>( 3 o => GetFontSize(o), 4 12); 5 6 public static double GetFontSize(BindableObject bindable) { 7 return (double)bindable.GetValue(FontSizeProperty); 8 }
是不是有些奇葩?
我在 http://www.cnblogs.com/xling/p/xamarin-forms-uwp-caliburn-micro.html 做过介绍, 但是伴随而来另外一个问题:View 无法找到对应的 ViewModel, 这里对它进行修正, 以使:
XXX.Views.Windows.XXXView 也可以对应到 XXX.ViewModels.XXXViewModel
做法就是通过获取 Type ,然后进行相应的替换。
1 private void FixCM(SimpleContainer container) { 2 var f = ViewLocator.LocateTypeForModelType; 3 ViewLocator.LocateTypeForModelType = (type, bindable, context) => { 4 return f(type, bindable, context ?? Device.OS) ?? f(type, bindable, context); 5 }; 6 7 var ps = string.Join("|", Enum.GetNames(typeof(TargetPlatform)).Select(p => string.Format(@"\.{0}", p))); 8 var rx = new Regex(string.Format("({0})$", ps)); 9 var f2 = ViewModelLocator.LocateForViewType; 10 ViewModelLocator.LocateForViewType = viewType => { 11 var vm = f2(viewType); 12 if (vm == null) { 13 if (rx.IsMatch(viewType.FullName)) {
14 var vmTypeName = rx.Replace(viewType.FullName, "ViewModel") 15 .Replace(".Views.", ".ViewModels."); 16 var vmType = Type.GetType(vmTypeName); 17 if (vmType != null) { 18 return container.GetInstance(vmType, null); 19 } 20 } 21 } 22 return vm; 23 }; 24 }
详见: https://github.com/gruan01/Xamarin.Forms.Lagou/blob/master/Lagou/Lagou/App.cs
如果直接写的 Android 项目,完成这个功能, 只需要改动一下布局文件,分分钟钟的事。但是在 XF 中, 要想达到这个目的, 只能通过自定义 Renderer 来实现。
Xamarin.Forms 1.5.1 之后,添加了对 Material Design 的支持:
http://www.cnblogs.com/xling/p/Material-Design-For-Xamarin-Forms.html
在 Material Design 下, TabbedPage 是通过 Tablayout + ViewPager 实现的, 这就给我提供了改动的机会(之前是能过 ActionBar 实现的, 不好改):
我们只需要获取 TabbedPageRenderer 中的 TabLayout 和 ViewPager , 在 Layout 的时候,交换一下它俩的显示位置就可以了:
[assembly: ExportRenderer(typeof(TabbedPage), typeof(TabbedPageRender))] namespace Lagou.Droid.Renders { public class TabbedPageRender : TabbedPageRenderer { private Android.Views.View formViewPager = null; private TabLayout tabLayout = null; protected override void OnElementChanged(ElementChangedEventArgs<TabbedPage> e) { base.OnElementChanged(e); this.formViewPager = this.GetChildAt(0); this.tabLayout = (TabLayout)this.GetChildAt(1); this.UpdateTabIcons(); //this.UpdateTabHeader(); } protected override void OnLayout(bool changed, int l, int t, int r, int b) { base.OnLayout(changed, l, t, r, b); // update layout , let tab on the bottom of the page // formViewPager upon tab. var w = r - 1; var h = b - t; if (w > 0 && h > 0) { int ypos = Math.Min(h, Math.Max(this.tabLayout.MeasuredHeight, this.tabLayout.MinimumHeight)); this.formViewPager.Layout(0, -ypos, r, b - ypos); this.tabLayout.Layout(l, h - ypos, r, b); } }
注意, 要使用 Material Design, MainActivity 必须继承自 FormsAppCompatActivity。
说到 FormsAppCompatActivity , 这又里又延伸到另外一个问题:
如上面代码所示,扩展了 TabbedPageRenderer ,但是结果发现这个自定义的 Renderer 并没有执行。这是什么情况?
先看一下 Xamarin.Forms.Platform.Android.dll 这个 dll 的结构:
它包括 Xamarin.Platform.Android 和 Xamarin.Platform.Android.AppCompat 这两个 namespace
AppCompat 下的这几个 Renderer ,在另外一个 namespace 下,也同样存在一份。
在看一下 Renderer 是怎么注册的:
MainActivity 的OnCreate 法中:
global::Xamarin.Forms.Forms.Init(this, bundle);
->>>
->>>
如最后这张图所示,RegisterAll 首先做的是:排序,保证 Xamarin.Forms.Platform.Android.dll 的 assembly 是 array 中的第一个。
然后遍历这个 assembly array, 对 assembly 中的自定义 Renderer 进行注册。
也就是说:后出现的 Renderer 会覆盖前面注册过的 Renderer.
我觉得这个地方设计的不合理。
假如引用了两个 DLL, 这两个DLL都有自定义的 ButtonRenderer, 运行时到底会执行哪个 Renderer , 我想那只有听天由命了!!!
上面我说 Material Design 的 MainActivity 必须继承自 FormsAppCompatActivity ,
这个 FormsAppCompatActivity 的 LoadApplication 方法中, 会调用 RegisterHandlerForDefaultRenderer ,将 AppCompat 中的这几个 Renderer 做为默认的 Renderer.
1 protected void LoadApplication(Xamarin.Forms.Application application) 2 { 3 if (!this.renderersAdded) 4 { 5 this.RegisterHandlerForDefaultRenderer(typeof (NavigationPage), typeof (NavigationPageRenderer), typeof (NavigationRenderer)); 6 this.RegisterHandlerForDefaultRenderer(typeof (TabbedPage), typeof (TabbedPageRenderer), typeof (TabbedRenderer)); 7 this.RegisterHandlerForDefaultRenderer(typeof (MasterDetailPage), typeof (MasterDetailPageRenderer), typeof (MasterDetailRenderer)); 8 this.RegisterHandlerForDefaultRenderer(typeof (Xamarin.Forms.Button), typeof (Xamarin.Forms.Platform.Android.AppCompat.ButtonRenderer), typeof (ButtonRenderer)); 9 this.RegisterHandlerForDefaultRenderer(typeof (Xamarin.Forms.Switch), typeof (Xamarin.Forms.Platform.Android.AppCompat.SwitchRenderer), typeof (SwitchRenderer)); 10 this.RegisterHandlerForDefaultRenderer(typeof (Picker), typeof (Xamarin.Forms.Platform.Android.AppCompat.PickerRenderer), typeof (PickerRenderer)); 11 this.RegisterHandlerForDefaultRenderer(typeof (Frame), typeof (Xamarin.Forms.Platform.Android.AppCompat.FrameRenderer), typeof (FrameRenderer)); 12 this.RegisterHandlerForDefaultRenderer(typeof (CarouselPage), typeof (Xamarin.Forms.Platform.Android.AppCompat.CarouselPageRenderer), typeof (CarouselPageRenderer)); 13 } 14 ...
而 LoadApplication 又必须出现在 Forms.Init 后面:
1 public class MainActivity : FormsAppCompatActivity { 2 protected override void OnCreate(Bundle bundle) { 3 base.OnCreate(bundle); 4 5 global::Xamarin.Forms.Forms.Init(this, bundle); 6 7 。。。 8 。。。 9 10 LoadApplication(new App(IoC.Get<SimpleContainer>()));
也就是说, 如果不巧你定义的就是 NavigationPage / TabbedPage / MasterDetailPage / Button / Switch / Picker / Frame / CarouselPage 的 Renderer, 那对不起, 在 FormsAppCompatActivity 下, 你写的 Renderer 会被取代!!!所以就出现了这个问题:
自定义的 Renderer 没有执行!
那如何解决呢? 当然是在调用 RegisterHandlerForDefaultRenderer 把自定义的 Renderer 给注册回去了!
只不过,这个方法是 Private 的, 无法直接调用, 但是不妨碍你使用反射。
看到图示中的图标没有? 全是字体!为啥用字体而不用图片,我就不说了。
TabbedPage 的图标,为其子 Page 的 Icon 属性, 当前并不支持字体。怎么办呢? 你可以从 TabbedPage 派生出一个控件,然后写对的 Renderer 就行了。
我不这样做,直接用 Attached Property:
1 public class AttachedFontIcon { 2 3 public static readonly BindableProperty GlyphProperty = 4 BindableProperty.CreateAttached<AttachedFontIcon, string>( 5 o => (string)o.GetValue(GlyphProperty), 6 string.Empty); 7 8 public static readonly BindableProperty FontFamilyProperty = 9 BindableProperty.CreateAttached<AttachedFontIcon, string>( 10 o => GetFontFamily(o), 11 string.Empty); 12 13 public static readonly BindableProperty FontSizeProperty = 14 BindableProperty.CreateAttached<AttachedFontIcon, double>( 15 //Bug, If direct GetValue without Function, xaml value can't convert to target type. 16 // Use Function will not have this issue. 17 o => GetFontSize(o), //(double)o.GetValue(FontSizeProperty), 18 12); 19 20 public static readonly BindableProperty ColorProperty = 21 BindableProperty.CreateAttached<AttachedFontIcon, Color>( 22 o => GetColor(o),//(Color)o.GetValue(ColorProperty), 23 Color.Default); 24 25 public static Color GetColor(BindableObject bindable) { 26 var color = (Color)bindable.GetValue(ColorProperty); 27 return color; 28 } 29 30 public static double GetFontSize(BindableObject bindable) { 31 return (double)bindable.GetValue(FontSizeProperty); 32 } 33 34 public static string GetFontFamily(BindableObject obj) { 35 return obj.GetValue(FontFamilyProperty) as string; 36 } 37 }
1 <?xml version="1.0" encoding="utf-8" ?> 2 <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" 3 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 4 x:Class="Lagou.Views.IndexView" 5 6 xmlns:local="clr-namespace:Lagou;assembly=Lagou" 7 local:AttachedFontIcon.Glyph="" 8 local:AttachedFontIcon.FontFamily="Fonts/FontAwesome.otf" 9 local:AttachedFontIcon.Color="Green" 10 local:AttachedFontIcon.FontSize="20" 11 >
然后在自定义的 TabbedPageRenderer 中,获取这些 Attached Property, 如果获取的到, 就根据这些值生成对应的字体图标。
1 private void UpdateTabIcons() { 2 var tabLayout = this.tabLayout; 3 if (tabLayout.TabCount != this.Element.Children.Count) 4 return; 5 6 for (int i = 0; i < this.Element.Children.Count; ++i) { 7 var page = this.Element.Children[i]; 8 if (string.IsNullOrEmpty(page.Icon)) { 9 var glyph = (string)page.GetValue(AttachedFontIcon.GlyphProperty); 10 if (!string.IsNullOrWhiteSpace(glyph)) { 11 12 SpannableStringBuilder sb = new SpannableStringBuilder(); 13 sb.Append(glyph); 14 15 var font = (string)page.GetValue(AttachedFontIcon.FontFamilyProperty); 16 if (!string.IsNullOrWhiteSpace(font)) { 17 var span = new CustomTypefaceSpan(font); 18 sb.SetSpan(span, 0, glyph.Length, SpanTypes.ExclusiveExclusive); 19 } 20 21 //var size = (int)(double)page.GetValue(AttachedFontIcon.FontSizeProperty); 22 sb.SetSpan(new AbsoluteSizeSpan(40), 0, glyph.Length, SpanTypes.ExclusiveExclusive); 23 24 sb.Append("\n"); 25 sb.Append(page.Title); 26 27 //Must set app:tabTextAppearance textAllCaps to false. 28 // See : tabs.xml, 29 //if not do this, span will not work not work 30 tabLayout.GetTabAt(i).SetText(sb); 31 } 32 } 33 } 34 }
可以看到, 这个图标,其实是通过 SpannableString 配合 TypefaceSpan 做出来的,它的本质就是一个文本而而。
需要注意的是, 如果要使 TypefaceSpan 在 TabLayout 的 Text 起作用,必须修改 textAllCaps 为 false.
具体参见:
https://github.com/gruan01/Xamarin.Forms.Lagou/blob/master/Lagou/Lagou.Droid/Renders/TabbedPageRender.cs
https://github.com/gruan01/Xamarin.Forms.Lagou/blob/master/Lagou/Lagou.Droid/Resources/layout/tabs.xml
https://github.com/gruan01/Xamarin.Forms.Lagou/blob/master/Lagou/Lagou.Droid/Resources/values/styles.xml
这是未加修饰的 UWP 的长相,坑爹是吧。
XF 都会有一个 NavigationPage ,所有的其它的 Page 都在这个 NavigationPage 中显示。
这个 NavigationPage 在 UWP 项目中,对应的是一个 自定义控件:PageControl ,派生自 ContentControl 。
但是 UWP 的 Xaml 文件都被编译成 XBF (Xaml Binary File) 了。 当前,还没有一个有效的工具能将 XBF 文件反编译成 XAML 文件。
所以无法得知这个 PageControl 到底包含些什么,还好,有 XamlSpy 这个工具: 从结构上可以看出,其实这个 PageControl 就一个标题,一个内容, 大胆写了一下它的样式:
1 <Style TargetType="xm:PageControl"> 2 <Setter Property="Template"> 3 <Setter.Value> 4 <ControlTemplate> 5 <Grid> 6 <Grid.RowDefinitions> 7 <RowDefinition Height="auto" /> 8 <RowDefinition MinHeight="100" /> 9 </Grid.RowDefinitions> 10 11 <Grid Grid.Row="0" Background="#00BCD4"> 12 <Grid.ColumnDefinitions> 13 <ColumnDefinition Width="auto" /> 14 <ColumnDefinition Width="auto" /> 15 <ColumnDefinition /> 16 </Grid.ColumnDefinitions> 17 18 <ToggleButton x:Name="splitViewToggle" Style="{StaticResource SplitViewTogglePaneButtonStyle}" Grid.Row="0" Grid.Column="0" /> 19 <ToggleButton x:Name="backToggle" Style="{StaticResource BackToggleButtonStyle}" Grid.Row="0" Grid.Column="1" Visibility="Collapsed" /> 20 <TextBlock Name="title" Text="{Binding Title}" Grid.Row="0" Grid.Column="2" VerticalAlignment="Center" /> 21 </Grid> 22 23 <ContentPresenter x:Name="presenter" Grid.Row="1" Grid.ColumnSpan="2" /> 24 25 </Grid> 26 </ControlTemplate> 27 </Setter.Value> 28 </Setter> 29 </Style>
标题,内容,外加两个 ToggleButton, 上图的效果就出来了。
用反编译工具可以看到 MasterDetailPage 的 NativeControl 是一个自定义的控件: MasterDetailControl。
同样不知道它怎么布局的。用 XamlSpy 查看,就发现两个有用的, 一个 SplitView 和一个 ToogleButton, 我们的目标就是把这个 ToogleButton 去掉。
1 <Style TargetType="xm:MasterDetailControl"> 2 <Style.Setters> 3 <Setter Property="Template"> 4 <Setter.Value> 5 <ControlTemplate> 6 <SplitView 7 x:Name="masterDetailSplitView" 8 OpenPaneLength="300" 9 > 10 <SplitView.Pane> 11 <Grid> 12 <ContentPresenter 13 x:Name="MasterPresenter" 14 HorizontalAlignment="Stretch" 15 VerticalAlignment="Stretch" 16 Content="{Binding Master, Converter={StaticResource renderConverter}}" /> 17 </Grid> 18 </SplitView.Pane> 19 20 <ContentPresenter 21 x:Name="DetailPresenter" 22 Content="{Binding Detail, Converter={StaticResource renderConverter}}" /> 23 24 </SplitView> 25 </ControlTemplate> 26 </Setter.Value> 27 </Setter> 28 </Style.Setters> 29 </Style>
注意 renderConverter,
<xm:ViewToRendererConverter x:Key="renderConverter" />
因为 Master, Detail 这两个数据只是 XF 的视图,UWP 并不识别它们,需要把它们用 ViewToRendererConverter 进行转换。
到此,第二个目标完成, 但是点那个 ToggleButton , SplitView 并没有反应。
在 MainPage 的构造函数添加它的 Load 事件处理程序:
1 private void MainPage_Loaded(object sender, Windows.UI.Xaml.RoutedEventArgs e) { 2 var btn = this.FindChildControl<ToggleButton>("splitViewToggle"); 3 if (btn != null) 4 btn.Click += Btn_Click; 5 6 var backBtn = this.FindChildControl<ToggleButton>("backToggle"); 7 if (backBtn != null) 8 backBtn.Click += BackBtn_Click; 9 } 10 11 private async void BackBtn_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e) { 12 await Lagou.App.Current.MainPage.Navigation.PopAsync(); 13 } 14 15 private void Btn_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e) { 16 var spv = this.FindChildControl<SplitView>("masterDetailSplitView"); 17 if (spv != null) 18 spv.IsPaneOpen = !spv.IsPaneOpen; 19 }
这两个 ToogleButton 什么时候显示、什么时候隐藏,在 自定义的 NavigationPageRender 中处理:
https://github.com/gruan01/Xamarin.Forms.Lagou/blob/master/Lagou/Lagou.UWP/Renders/NavigationPageRender.cs
TabbedPage 在 UWP 中的 NativeControl 就是 Pivot.
所以先重写 Pivot 的样式,太长,就不贴了:
https://github.com/gruan01/Xamarin.Forms.Lagou/blob/master/Lagou/Lagou.UWP/Pivot.xaml
然后把这个 Pivot.xaml 加入 ResourceDictionary 中,运行,结果发现,根本就没有应用这个新加的样式。
看一下 TabbedPageRenderer , 原来是指定了 Style 为 TabbedPageStyle。我尝试重新指定 Style , 但是运行就报什么 DesiredSize 异常。
没有办法,只好照着原来的 Renderer 重新写了一个:
https://github.com/gruan01/Xamarin.Forms.Lagou/blob/master/Lagou/Lagou.UWP/Renders/TabbedPageRender.cs
并指定 ItemTemplate 和 HeaderTemplate
1 <DataTemplate x:Key="TabbedPageItemDataTemplate"> 2 <render:TabbedPagePresenter Content="{Binding Converter={StaticResource pageRenderConverter}}" /> 3 </DataTemplate> 4 5 <DataTemplate x:Key="TabbedPageHeaderDataTemplate"> 6 <StackPanel> 7 <ContentControl Content="{Binding Converter={StaticResource tabbedPageIconConverter}}" HorizontalContentAlignment="Center" /> 8 <TextBlock Text="{Binding Title}" FontSize="12" /> 9 </StackPanel> 10 </DataTemplate>
至于为什么使用 TabbedPagePresenter, 我没有看太懂, 只是原样抄出来而已(这个类为 internal 的类)
Renderer 的设计显的有些封闭, 不够开放,太多 private / internal 了,要想自定义 Renderer 可不是件容易的事!
---------------
好啦, 让我看到你们的膝盖!
另外:
求职:架构师 、开发经理
10年工作经验
C# / MVC / WPF / WCF / JS / CSS / SQL 应有尽有