这是我们比较Kivy,Xamarin.Forms和React Native的系列文章中的第二篇。 在其中,我将尝试使用Xamarin.Forms编写相同的任务计划程序。 我将了解如何做以及必须面对的问题。
我不会重复传统知识;可以在第一篇文章中看到:
Kivy。 Xamarin 反应本机。 三个框架-一个实验第三部分是关于React Native:
Kivy。 Xamarin 反应本机。 三个框架-一个实验(第3部分)首先,我将简要介绍Xamarin.Forms平台以及如何解决该任务。 Xamarin.Forms是Xamarin.iOs和Xamarin.Android的附加组件。 组装后,常规部分将“部署”到标准本机控件,因此从本质上讲,您将获得所有受支持平台的完全本机应用程序。
Xamarin.Forms的语法与WPF的语法非常接近,通用部分用.NET Standard编写。 因此,您有机会在开发应用程序时使用MVVM方法,并可以访问为.NET Standard和NuGet编写的大量第三方库,您可以在Xamarin.Forms应用程序中安全地使用它们。
此处的应用程序源代码可在
GitHub上
找到 。
因此,让我们创建一个空的Xamarin.Forms应用程序并开始使用。 我们将有一个简单的数据模型,其中只有两个Note和Project类:
public class Note { public string UserIconPath { get; set; } public string UserName { get; set; } public DateTime EditTime { get; set; } public string Text { get; set; } } public class Project { public string Name { get; set; } public ObservableCollection<Note> Notes { get; set; } public Project() { Notes = new ObservableCollection<Note>(); } }
我将尝试坚持MVVM方法,但是我将不使用任何特殊的库,以免使代码复杂化。 所有模型类和视图模型都将实现INotifyPropertyChanged接口。 为了简洁起见,我将在给定的代码示例中删除其实现。
我们将看到的第一个屏幕是项目列表,这些项目具有创建新项目或删除当前项目的能力。 让我们为他建模:
public class MainViewModel { public ObservableCollection<Project> Projects { get; set; } public MainViewModel() { Projects = Project.GetTestProjects(); } public void AddNewProject(string name) { Project project = new Project() { Name = name }; Projects.Add(project); } public void DeleteProject(Project project) { Projects.Remove(project); } }
屏幕代码本身:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:local="clr-namespace:TodoList.View" x:Class="TodoList.View.ProjectsPage"> <ContentPage.ToolbarItems> <ToolbarItem Clicked="AddNew_Clicked" Icon="plus.png"/> </ContentPage.ToolbarItems> <ListView ItemsSource="{Binding Projects}" ItemTapped="List_ItemTapped"> <ListView.ItemTemplate> <DataTemplate> <TextCell Text="{Binding Name}" TextColor="Black"> <TextCell.ContextActions> <MenuItem Clicked="DeleteItem_Clicked" IsDestructive="true" CommandParameter="{Binding .}" Text="Delete"/> </TextCell.ContextActions> </TextCell> </DataTemplate> </ListView.ItemTemplate> </ListView> </ContentPage>
标记非常简单,我只想讲的是删除项目的滑动按钮的实现。 在ListView中有ContextActions的概念,如果设置了它,则在iOS中将通过滑动,在Android中(通过长按)实现它们。 此方法在Xamarin.Forms中实现,因为它是每个平台固有的。 但是,如果要在android中滑动,则需要在android的本机部分中用手实现它。 我没有任务要花很多时间,所以我对标准方法很满意:)结果,iOS中的滑动和Android中的上下文菜单的实现非常简单:
<TextCell.ContextActions> <MenuItem Clicked="DeleteItem_Clicked" IsDestructive="true" CommandParameter="{Binding .}" Text="Delete"/> </TextCell.ContextActions>
替换测试数据,我们得到以下列表:

现在,让我们继续进行事件处理程序。 让我们从一个简单的开始-删除一个项目:
MainViewModel ViewModel { get { return BindingContext as MainViewModel; } } async Task DeleteItem_Clicked(object sender, EventArgs e) { MenuItem menuItem = sender as MenuItem; if (menuItem == null) return; Project project = menuItem.CommandParameter as Project; if (project == null) return; bool answer = await DisplayAlert("Are you sure?", string.Format("Would you like to remove the {0} project", project.Name), "Yes", "No"); if(answer) ViewModel.DeleteProject(project); }
在不询问用户的情况下删除某些内容是不好的,并且在Xamarin.Forms中,使用标准DisplayAlert方法很容易做到这一点。 调用后,将出现以下窗口:

该窗口来自iO。 Android将拥有自己的类似窗口版本。
接下来,我们将实现一个新项目的添加。 似乎是通过类推完成的,但是在Xamarin.Forms中,没有实现类似于我确认删除的对话框的对话,但是允许您输入文本。 有两种可能的解决方案:
- 编写自己的服务,以引发本机对话框;
- 在Xamarin.Forms方面实现某种解决方法。
我不想浪费时间通过本机进行对话,所以我决定使用第二种方法,该方法的实现是从线程中获取的:
如何创建一个简单的InputBox对话框? ,即Task InputBox(导航导航)方法。
async Task AddNew_Clicked(object sender, EventArgs e) { string result = await InputBox(this.Navigation); if (result == null) return; ViewModel.AddNewProject(result); }
现在,我们将逐行处理打开项目:
void List_ItemTapped(object sender, Xamarin.Forms.ItemTappedEventArgs e) { Project project = e.Item as Project; if (project == null) return; this.Navigation.PushAsync(new NotesPage() { BindingContext = new ProjectViewModel(project) }); }
从上面的代码中可以看到,要转到项目窗口,我们需要它的视图模型和页面的window对象。
我想谈谈导航。 Navigation属性在VisualElement类中定义,并允许您在应用程序的任何视图中使用导航面板,而无需用手将其滚动到那里。 但是,要使这种方法起作用,您仍然需要自己创建此面板。 因此,在App.xaml.cs中,我们编写:
NavigationPage navigation = new NavigationPage(); navigation.PushAsync(new View.ProjectsPage() { BindingContext = new MainViewModel() }); MainPage = navigation;
ProjectsPage正是我现在描述的窗口。
带有注释的窗口与带有项目的窗口非常相似,因此我将不对其进行详细描述,仅关注有趣的细微差别。
事实证明,此窗口的布局更为复杂,因为每一行都应显示更多信息:
笔记视图 <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="TodoList.View.NotesPage" xmlns:local="clr-namespace:TodoList.View" xmlns:utils="clr-namespace:TodoList.Utils" Title="{Binding Project.Name}"> <ContentPage.Resources> <ResourceDictionary> <utils:PathToImageConverter x:Key="PathToImageConverter"/> </ResourceDictionary> </ContentPage.Resources> <ContentPage.ToolbarItems> <ToolbarItem Clicked="AddNew_Clicked" Icon="plus.png"/> </ContentPage.ToolbarItems> <ListView ItemsSource="{Binding Project.Notes}" x:Name="list" ItemTapped="List_ItemTapped" HasUnevenRows="True"> <ListView.ItemTemplate> <DataTemplate> <ViewCell> <local:MyCellGrid Margin="5"> <local:MyCellGrid.RowDefinitions> <RowDefinition Height="40"/> <RowDefinition Height="*"/> </local:MyCellGrid.RowDefinitions> <local:MyCellGrid.ColumnDefinitions> <ColumnDefinition Width="40"/> <ColumnDefinition Width="*"/> <ColumnDefinition Width="40"/> </local:MyCellGrid.ColumnDefinitions> <Image Grid.Row="0" Grid.Column="0" Source="{Binding UserIconPath, Converter={StaticResource PathToImageConverter}}" /> <StackLayout Grid.Row="0" Grid.Column="1"> <Label Text="{Binding UserName}" FontAttributes="Bold"/> <Label Text="{Binding EditTime}"/> </StackLayout> <Button Grid.Row="0" Grid.Column="2" BackgroundColor="Transparent" Image="menu.png" Margin="5" HorizontalOptions="FillAndExpand" Clicked="RowMenu_Clicked"/> <local:MyLabel Grid.Row="1" Grid.Column="1" Margin="0,10,0,0" Grid.ColumnSpan="2" Text="{Binding Text}"/> </local:MyCellGrid> <ViewCell.ContextActions> <MenuItem Clicked="DeleteItem_Clicked" IsDestructive="true" CommandParameter="{Binding .}" Text="Delete"/> </ViewCell.ContextActions> </ViewCell> </DataTemplate> </ListView.ItemTemplate> </ListView> </ContentPage>
在窗口的内容中,我们再次有一个ListView,附加到注释集合中。 但是,我们希望内容中单元格的高度不超过150,为此我们将HasUnevenRows =“ True”设置为使ListView允许单元格按照其要求占用尽可能多的空间。 但是在这种情况下,这些行可以请求超过150的高度,并且ListView将允许这样显示它们。 为了避免在单元格中出现这种情况,我将继承人用于“网格”面板:MyCellGrid。 测量操作上的此面板要求内部元素的高度,如果更大,则返回150:
public class MyCellGrid : Grid { protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint) { SizeRequest sizeRequest = base.OnMeasure(widthConstraint, heightConstraint); if (sizeRequest.Request.Height <= 150) return sizeRequest; return new SizeRequest(new Size() { Width = sizeRequest.Request.Width, Height = 150 }); } }
由于根据TOR,除了点击和滑动外,我们还需要能够编辑和删除通过单击行角处的按钮打开的菜单,因此我们将此按钮添加到单元格模板并订阅其上的点击。 在这种情况下,如果用户单击该按钮,则它将拦截手势,并且我们将不会收到单击该行的事件。
<Button Grid.Row="0" Grid.Column="2" BackgroundColor="Transparent" Image="menu.png" Margin="5" HorizontalOptions="FillAndExpand" Clicked="RowMenu_Clicked"/>
使用测试数据,我们的表单如下所示:

以这种形式处理用户操作完全类似于为项目列表窗口编写的操作。 我只想通过该行拐角处的按钮在上下文菜单上停止。 起初,我认为我会在Xamarin.Forms级别上做到这一点,而不会出现任何问题。
确实,我们只需要创建类似以下内容的视图:
<StackLayout> <Button Text=”Edit”/> <Button Text=”Delete”/> </StackLayout>
并将其显示在按钮旁边。 但是,问题在于我们无法确切知道“按钮旁边”的位置。 此上下文菜单应位于ListView的顶部,并且在打开时应位于窗口的坐标中。 为此,您需要知道按钮相对于窗口的坐标。 我们只能获得相对于位于ListView中的内部ScrollView的按钮的坐标。 因此,当直线不移动时,一切都很好,但是滚动直线时,在计算坐标时必须考虑发生了多少滚动。 ListView没有提供滚动值。 因此,必须将其从本机中拉出来,而我确实不希望这样做。 因此,我决定采用一种更标准和更简单的方法:显示标准系统上下文菜单。 结果,按钮单击处理程序将显示以下内容:
async Task RowMenu_Clicked(object sender, System.EventArgs e) { string action = await DisplayActionSheet("Note action:", "Cancel", null, "Edit", "Delete"); if (action == null) return; BindableObject bindableSender = sender as BindableObject; if(bindableSender != null) { Note note = bindableSender.BindingContext as Note; if (action == "Edit") { EditNote(note); } else if(action == "Delete") { await DeleteNote(note); } } }
调用DisplayActionSheet方法仅显示常规上下文菜单:

如果您注意到,便笺的文本将显示在MyLabel控件中,而不是常规的Label中。 这样做是为了什么。 当用户更改便笺的文本时,将触发活页夹,新文本自动到达Label中。 但是,Xamarin.Forms不会同时重新计算单元格大小。 Xamarin开发人员声称这是相当昂贵的操作。 是的,并且ListView本身没有任何方法可以重述其大小,InvalidateLayout也无济于事。 他们唯一拥有的是Cell对象的ForceUpdateSize方法。 因此,为了在正确的时间到达并拖动它,我写了我的Label继承人,并为每次文本更改使用了此方法:
public class MyLabel : Label { protected override void OnPropertyChanged([CallerMemberName] string propertyName = null) { base.OnPropertyChanged(propertyName); if (propertyName == "Text") { ((this.Parent as MyCellGrid).Parent as Cell).ForceUpdateSize(); } } }
现在,在编辑便笺后,ListView将自动调整单元格大小以适合新文本。
编辑或创建新便笺时,将打开一个窗口,其中的内容为“编辑器”,工具栏上的“保存”按钮为:

该窗口与TK中的窗口略有不同:底部缺少圆形按钮。 如果仅将其放置在编辑器的顶部,则离开键盘将其阻塞。 同时,我没有找到一个很好的解决方案来移动它,也没有快速搜索它的本机。 因此,我将其删除,只在顶部面板中保留了“保存”按钮。 该窗口本身非常简单,因此我将省略其描述。
最后我想说什么。
Xamarin.Forms非常适合那些熟悉.NET基础结构并使用了很长时间的人。 他们将不必升级到新的IDE和框架。 如您所见,该应用程序代码与任何其他基于XAML的应用程序的代码没有太大不同。 此外,Xamarin允许您在Visual Studio for Windows中开发和构建iOS应用程序。 在开发用于测试和构建最终应用程序的最终应用程序时,您需要连接至MacOS计算机。 没有它就可以完成库。
为了开始在Xamarin.Forms上编写应用程序,您无需在控制台上打红眼。 只需安装Visual Studio并编写应用程序。 其他一切都已经照顾好了。 同时,无论Microsoft与付费产品如何关联,Xamarin是免费的,并且有Visual Studio的免费版本。
Xamarin.Forms在后台使用.NET Standard的事实使您可以访问一堆为此编写的库,这将使开发应用程序时的工作变得更轻松。
如果您要实现特定于平台的内容,则Xamarin.Forms允许您轻松地在应用程序的本机部分中添加某些内容。 在那里,您获得相同的C#,但是API是每个平台固有的。
但是,当然有一些缺点。
通用部分提供的API很少,因为它仅包含所有平台通用的内容。 例如,从我的示例中可以看出,所有平台都包含警报消息和上下文菜单,并且Xamarin.Forms中提供了此功能。 但是,允许您输入文本的标准菜单仅在iOS中可用,因此在Xamarin.Forms中则没有。
在组件的使用中发现了类似的限制。 可以做的事情,不可能的事情。 相同的滑动操作才能删除项目或注释,仅在iOS中有效。 在Android中,此上下文操作将显示为长按时出现的菜单。 如果您想在android上滑动,则欢迎进入android部分并用手书写。
当然还有性能。 在任何情况下,Xamarin.Forms上应用程序的速度都将低于本机应用程序的速度。 因此,Microsoft本身说,如果您需要一个在设计和性能要求方面没有任何多余要求的应用程序,那么Xamarin.Forms适合您。 如果您需要漂亮或快速,那么您应该已经习惯了本机。 幸运的是,Xamarin还具有可通过其本机平台API立即运行的本机版本,并且比表单运行得更快。