像现实生活中的应用程序
理想情况下,用户在终止并重新启动应用程序时不应该知道。应用程序体验应该是连续且无缝的。即使程序没有一直运行,一个半月进入的条目从未完成也应该在一周后处于相同的状态。
NoteTaker程序允许用户记录由标题和一些文本组成的注释。因为可能有相当多的这些音符,并且它们有可能变得很长,所以它们不会存储在属性字典中。相反,该程序使用了第20章“异步和文件I / O”中TextFileAsync程序中演示的IFileHelper接口和FileHelper类。每个注释都是一个单独的文件。与TextFileAsync一样,NoteTaker解决方案还包含Xamarin.FormsBook.Platform解决方案中的所有项目以及对这些库项目的引用。
NoteTaker的结构与本章前面的DataTransfer程序非常相似,其页面名为NoteTakerHomePage和NoteTakerNotePage。
主页包含一个支配页面的ItemsView和一个Add按钮。此添加按钮是一个ToolbarItem,它采用iOS,Android和Windows Phone屏幕右上角的加号形式:
按下该按钮会使程序导航到NoteTakerNotePage。 顶部是标题的条目,但是大部分页面都被编辑器占用了注释本身的文本。 您现在可以输入标题并注意:
没有必要输入标题。 如果没有,则构造一个由文本开头组成的标识符。 也没有必要输入注释文本。 注释可以仅包含标题。 (在编写本章时,Windows运行时编辑器未正确包装文本。)
如果标题或注释不为空白,则该注释将被视为有效注释。 当您使用导航栏或屏幕底部的标准后退按钮返回主页时,新注释将添加到ListView:
您现在可以通过点击ListView中的条目添加更多新笔记或编辑现有笔记。 ListView点击导航到与添加按钮相同的页面,但请注意第二页上的标题属性现在是“编辑注释”而不是“新注释”:
您现在可以更改注释并返回主页。此页面上还有两个工具栏项:第一个是“取消”按钮,允许您放弃所做的任何编辑。警报询问您是否确定。您也可以点击删除项目以删除注释,同时提供确认提醒。
该程序的一个棘手方面是“取消”按钮。假设你正在编辑一个音符,你会分心,最终程序终止。下次启动程序时,您应该返回编辑屏幕并查看编辑内容。如果您随后调用“取消”命令,则应放弃编辑。
这意味着当正在编辑注释时暂停应用程序时,注释必须以两种不同的形式保存:预编辑注释和带编辑的注释。只有当NoteTakerNotePage调用其OnDisappearing覆盖时,程序才会通过将每个音符保存到文件来处理此问题。 (但是,当程序终止时,当页面调用OnDisappearing时,某些特殊考虑需要适应iOS中的情况。)Note对象的文件版本是没有编辑的文件版本。编辑后的版本由Entry和Editor的当前内容反映出来; NoteTakerNotePage将这两个文本字符串保存在其IPersistantPage实现的Save方法中。
Note类通过从Xamarin.FormsBook.Toolkit库中的ViewModelBase派生来实现INotifyPropertyChanged。 该类定义了四个公共属性:Filename,Title,Text和Identifier,它们与Title相同或者Text的前30个字符,被截断以仅显示完整的单词。 Filename属性是从构造函数设置的,永远不会更改:
public class Note : ViewModelBase, IEquatable<Note>
{
string title, text, identifier;
FileHelper fileHelper = new FileHelper();
public Note(string filename)
{
Filename = filename;
}
public string Filename { private set; get; }
public string Title
{
set
{
if (SetProperty(ref title, value))
{
Identifier = MakeIdentifier();
}
}
get { return title; }
}
public string Text
{
set
{
if (SetProperty(ref text, value) && String.IsNullOrWhiteSpace(Title))
{
Identifier = MakeIdentifier();
}
}
get { return text; }
}
public string Identifier
{
private set { SetProperty(ref identifier, value); }
get { return identifier; }
}
string MakeIdentifier()
{
if (!String.IsNullOrWhiteSpace(this.Title))
return Title;
int truncationLength = 30;
if (Text == null || Text.Length <= truncationLength)
{
return Text;
}
string truncated = Text.Substring(0, truncationLength);
int index = truncated.LastIndexOf(' ');
if (index != -1)
truncated = truncated.Substring(0, index);
return truncated;
}
public Task SaveAsync()
{
string text = Title + Environment.NewLine + Text;
return fileHelper.WriteTextAsync(Filename, text);
}
public async Task LoadAsync()
{
string text = await fileHelper.ReadTextAsync(Filename);
// Break string into Title and Text.
int index = text.IndexOf(Environment.NewLine);
Title = text.Substring(0, index);
Text = text.Substring(index + Environment.NewLine.Length);
}
public async Task DeleteAsync()
{
await fileHelper.DeleteAsync(Filename);
}
public bool Equals(Note other)
{
return other == null ? false : Filename == other.Filename;
}
}
Note类还定义了保存,加载或删除与特定类实例关联的文件的方法。 该文件的第一行是Title属性,文件的其余部分是Text属性。
在大多数情况下,Note文件和Note类的实例之间存在一对一的对应关系。 但是,如果调用DeleteAsync方法,则Note对象仍然存在,但文件不存在。 (但是,正如您将看到的那样,对调用了DeleteAsync方法的Note对象的所有引用都会快速分离,并且该对象可以进行垃圾回收。)
程序未运行时,程序不会保留这些文件的列表。 相反,NoteFolder类获取应用程序本地存储中文件扩展名为“.note”的所有文件,并从这些文件创建一个Note对象集合:
public class NoteFolder
{
public NoteFolder()
{
this.Notes = new ObservableCollection<Note>();
GetFilesAsync();
}
public ObservableCollection<Note> Notes { private set; get; }
async void GetFilesAsync()
{
FileHelper fileHelper = new FileHelper();
// Sort the filenames.
IEnumerable<string> filenames =
from filename in await fileHelper.GetFilesAsync()
where filename.EndsWith(".note")
orderby (filename)
select filename;
// Store them in the Notes collection.
foreach (string filename in filenames)
{
Note note = new Note(filename);
await note.LoadAsync();
Notes.Add(note);
}
}
}
正如您将看到的,文件名是在首次创建注释时由DateTime对象构造的,包括年份,后面是月,日和时间,这意味着当这些注释文件按文件名排序时,它们 以与创建它们相同的顺序出现在集合中。
App类实例化NoteFolder并使其可用作公共属性。 App派生自MultiPageRestorableApp,因此它使用NoteTakerHomePage类型调用Startup,并通过调用基类实现来实现OnSleep重写:
public class App : MultiPageRestorableApp
{
public App()
{
// This loads all the existing .note files.
NoteFolder = new NoteFolder();
// Make call to method in MultiPageRestorableApp.
Startup(typeof(NoteTakerHomePage));
}
public NoteFolder NoteFolder { private set; get; }
protected override void OnSleep()
{
// Required call when deriving from MultiPageRestorableApp.
base.OnSleep();
}
// Special processing for iOS.
protected override void OnResume()
{
NoteTakerNotePage notePage =
((NavigationPage)MainPage).CurrentPage as NoteTakerNotePage;
if (notePage != null)
notePage.OnResume();
}
}
App类还会覆盖OnResume方法。 如果NoteTakerNotePage当前处于活动状态,则该方法将在注释页面中调用OnResume方法。 这是iOS的一些特殊处理。 正如您将看到的,NoteTakerNotePage在OnDisappearing覆盖期间将Note对象保存到文件中,但如果OnDisappearing覆盖指示应用程序正在终止,则不应该这样做。
NoteTakerHomePage的XAML文件实例化ListView以显示所有Note对象。 ItemsSource绑定到存储在App类中的NoteFolder的Notes集合。 每个Note对象都显示在ListView中,并带有Identifier属性:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="NoteTaker.NoteTakerHomePage"
Title="Note Taker">
<ListView ItemsSource="{Binding Source={x:Static Application.Current},
Path=NoteFolder.Notes}"
ItemSelected="OnListViewItemSelected"
VerticalOptions="FillAndExpand">
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Text="{Binding Identifier}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<ContentPage.ToolbarItems>
<ToolbarItem Name="Add Note"
Order="Primary"
Activated="OnAddNoteActivated">
<ToolbarItem.Icon>
<OnPlatform x:TypeArguments="FileImageSource"
iOS="new.png"
Android="ic_action_new.png"
WinPhone="Images/add.png" />
</ToolbarItem.Icon>
</ToolbarItem>
</ContentPage.ToolbarItems>
</ContentPage>
代码隐藏文件专门用于处理两个事件:ListView的ItemSelected事件用于编辑现有的Note,而ToolbarItem的Activated事件用于创建新的Note:
partial class NoteTakerHomePage : ContentPage
{
public NoteTakerHomePage()
{
InitializeComponent();
}
async void OnListViewItemSelected(object sender, SelectedItemChangedEventArgs args)
{
if (args.SelectedItem != null)
{
// Deselect the item.
ListView listView = (ListView)sender;
listView.SelectedItem = null;
// Navigate to NotePage.
await Navigation.PushAsync(new NoteTakerNotePage
{
Note = (Note)args.SelectedItem,
IsNoteEdit = true
});
}
}
async void OnAddNoteActivated(object sender, EventArgs args)
{
// Create unique filename.
DateTime dateTime = DateTime.UtcNow;
string filename = dateTime.ToString("yyyyMMddHHmmssfff") + ".note";
// Navigate to NotePage.
await Navigation.PushAsync(new NoteTakerNotePage
{
Note = new Note(filename),
IsNoteEdit = false
});
}
}
在这两种情况下,事件处理程序都实例化NoteTakerNotePage,设置两个属性,然后导航到该页面。 对于新注释,构造文件名并实例化Note对象。 对于现有注释,Note对象只是ListView中的选定项。 请注意,新注释具有文件名但尚未保存到文件中,或者成为NoteFolder中Notes集合的一部分。
NoteTakerNotePage的XAML文件有一个条目和编辑器,用于输入注释的标题和文本。 这些元素的Text属性上的数据绑定意味着页面的BindingContext是一个Note对象:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="NoteTaker.NoteTakerNotePage"
Title="New Note">
<StackLayout>
<Label Text="Title:" />
<Entry Text="{Binding Title}"
Placeholder="Title (optional)" />
<Label Text="Note:" />
<Editor Text="{Binding Text}"
Keyboard="Text"
VerticalOptions="FillAndExpand" />
</StackLayout>
<ContentPage.ToolbarItems>
<ToolbarItem Name="Cancel"
Order="Primary"
Activated="OnCancelActivated">
<ToolbarItem.Icon>
<OnPlatform x:TypeArguments="FileImageSource"
iOS="cancel.png"
Android="ic_action_cancel.png"
WinPhone="Images/cancel.png" />
</ToolbarItem.Icon>
</ToolbarItem>
<ToolbarItem Name="Delete"
Order="Primary"
Activated="OnDeleteActivated">
<ToolbarItem.Icon>
<OnPlatform x:TypeArguments="FileImageSource"
iOS="discard.png"
Android="ic_action_discard.png"
WinPhone="Images/delete.png" />
</ToolbarItem.Icon>
</ToolbarItem>
</ContentPage.ToolbarItems>
</ContentPage>
只有在编辑现有注释时,才能看到朝向底部的两个ToolbarItem元素。 从主页设置IsNoteEdit属性时,代码隐藏文件中将删除这些工具栏项。 该代码还会更改页面的Title属性。 Note属性的set访问器负责设置页面的BindingContext:
public partial class NoteTakerNotePage : ContentPage, IPersistentPage
{
Note note;
bool isNoteEdit;
__
public NoteTakerNotePage()
{
InitializeComponent();
}
public Note Note
{
set
{
note = value;
BindingContext = note;
}
get { return note; }
}
public bool IsNoteEdit
{
set
{
isNoteEdit = value;
Title = IsNoteEdit ? "Edit Note" : "New Note";
// No toolbar items if it's a new Note!
if (!IsNoteEdit)
{
ToolbarItems.Clear();
}
}
get { return isNoteEdit; }
}
__
}
NoteTakerNotePage类实现IPersistentPage接口,这意味着它具有名为Save and Restore的方法,用于保存和恢复页面状态。 这些方法使用Properties字典来保存和恢复Note的三个属性,这三个属性定义Note对象 - 文件名,标题和文本属性 - 以及NoteTakerNotePage的IsNoteEdit属性。 这是当前编辑状态下的Note对象:
public partial class NoteTakerNotePage : ContentPage, IPersistentPage
{
__
// Special field for iOS.
bool isInSleepState;
__
// IPersistent implementation
public void Save(string prefix)
{
// Special code for iOS.
isInSleepState = true;
Application app = Application.Current;
app.Properties["fileName"] = Note.Filename;
app.Properties["title"] = Note.Title;
app.Properties["text"] = Note.Text;
app.Properties["isNoteEdit"] = IsNoteEdit;
}
public void Restore(string prefix)
{
Application app = Application.Current;
// Create a new Note object.
Note note = new Note((string)app.Properties["fileName"])
{
Title = (string)app.Properties["title"],
Text = (string)app.Properties["text"]
};
// Set the properties of this class.
Note = note;
IsNoteEdit = (bool)app.Properties["isNoteEdit"];
}
// Special code for iOS.
public void OnResume()
{
isInSleepState = false;
}
__
}
该类还定义了一个名为OnResume的方法,该方法是从App类调用的。 因此,当应用程序被挂起时,isInSleepState字段为true。
isInSleepState字段的目的是避免在调用OnDisappearing覆盖时将Note保存到文件,因为应用程序在iOS下终止。 将此Note对象保存到文件将不允许用户稍后通过按此页面上的“取消”按钮放弃对“注释”的编辑。
如果OnDisappearing覆盖指示用户正在返回主页 - 就像在此应用程序中那样 - 那么Note对象可以保存到文件中,并可能添加到NoteFolder中的Notes集合中:
public partial class NoteTakerNotePage : ContentPage, IPersistentPage
{
__
async protected override void OnDisappearing()
{
base.OnDisappearing();
// Special code for iOS:
// Do not save note when program is terminating.
if (isInSleepState)
return;
// Only save the note if there's some text somewhere.
if (!String.IsNullOrWhiteSpace(Note.Title) ||
!String.IsNullOrWhiteSpace(Note.Text))
{
// Save the note to a file.
await Note.SaveAsync();
// Add it to the collection if it's a new note.
NoteFolder noteFolder = ((App)App.Current).NoteFolder;
// IndexOf method finds match based on Filename property
// based on implementation of IEquatable in Note.
int index = noteFolder.Notes.IndexOf(note);
if (index == -1)
{
// No match -- add it.
noteFolder.Notes.Add(note);
}
else
{
// Match -- replace it.
noteFolder.Notes[index] = note;
}
}
}
__
}
Note类实现IEquatable接口,如果它们的Filename属性相同,则定义两个Note对象相等。 OnDisappearing覆盖依赖于相等的定义,以避免在集合中添加Note对象(如果已存在具有相同Filename属性的另一个)。
最后,NoteTakerNotePage代码隐藏文件具有两个ToolbarItem元素的处理程序。 在这两种情况下,处理都始于调用DisplayAlert以获取用户确认,并从文件中重新加载Note对象(有效地覆盖任何编辑),或者删除文件并将其从Notes集合中删除:
public partial class NoteTakerNotePage : ContentPage, IPersistentPage
{
__
async void OnCancelActivated(object sender, EventArgs args)
{
if (await DisplayAlert("Note Taker", "Cancel note edit?",
"Yes", "No"))
{
// Reload note.
await Note.LoadAsync();
// Return to home page.
await Navigation.PopAsync();
}
}
async void OnDeleteActivated(object sender, EventArgs args)
{
if (await DisplayAlert("Note Taker", "Delete this note?",
"Yes", "No"))
{
// Delete Note file and remove from collection.
await Note.DeleteAsync();
((App)App.Current).NoteFolder.Notes.Remove(Note);
// Wipe out Entry and Editor so the Note
// won't be saved during OnDisappearing.
Note.Title = "";
Note.Text = "";
// Return to home page.
await Navigation.PopAsync();
}
}
}
当然,这不是编写这样的程序的唯一方法。 可以将许多用于创建,编辑和删除笔记的逻辑移动到AppData中,并使其成为合适的ViewModel。 AppData可能需要一个名为CurrentNote的Note类型的新属性,以及一些类型为ICommand的属性,用于绑定到每个ToolbarItem元素的Command属性。
一些程序员甚至尝试将页面导航逻辑移动到ViewModels中,但不是每个人都同意这是一种适合MVVM的方法。 页面是用户界面的一部分,因此是View的一部分吗? 或者页面真的更像是相关数据项的集合?
这些哲学问题可能会变得更加令人烦恼,因为Xamarin.Forms中的页面类型的种类将在下一章中进行探讨。