第二十四章:页面导航(十七)

简介: 像现实生活中的应用程序理想情况下,用户在终止并重新启动应用程序时不应该知道。应用程序体验应该是连续且无缝的。即使程序没有一直运行,一个半月进入的条目从未完成也应该在一周后处于相同的状态。NoteTaker程序允许用户记录由标题和一些文本组成的注释。

像现实生活中的应用程序
理想情况下,用户在终止并重新启动应用程序时不应该知道。应用程序体验应该是连续且无缝的。即使程序没有一直运行,一个半月进入的条目从未完成也应该在一周后处于相同的状态。
NoteTaker程序允许用户记录由标题和一些文本组成的注释。因为可能有相当多的这些音符,并且它们有可能变得很长,所以它们不会存储在属性字典中。相反,该程序使用了第20章“异步和文件I / O”中TextFileAsync程序中演示的IFileHelper接口和FileHelper类。每个注释都是一个单独的文件。与TextFileAsync一样,NoteTaker解决方案还包含Xamarin.FormsBook.Platform解决方案中的所有项目以及对这些库项目的引用。
NoteTaker的结构与本章前面的DataTransfer程序非常相似,其页面名为NoteTakerHomePage和NoteTakerNotePage。
主页包含一个支配页面的ItemsView和一个Add按钮。此添加按钮是一个ToolbarItem,它采用iOS,Android和Windows Phone屏幕右上角的加号形式:
2019_05_04_134348
按下该按钮会使程序导航到NoteTakerNotePage。 顶部是标题的条目,但是大部分页面都被编辑器占用了注释本身的文本。 您现在可以输入标题并注意:
2019_05_04_134446
没有必要输入标题。 如果没有,则构造一个由文本开头组成的标识符。 也没有必要输入注释文本。 注释可以仅包含标题。 (在编写本章时,Windows运行时编辑器未正确包装文本。)
如果标题或注释不为空白,则该注释将被视为有效注释。 当您使用导航栏或屏幕底部的标准后退按钮返回主页时,新注释将添加到ListView:
2019_05_04_134539
您现在可以通过点击ListView中的条目添加更多新笔记或编辑现有笔记。 ListView点击导航到与添加按钮相同的页面,但请注意第二页上的标题属性现在是“编辑注释”而不是“新注释”:
2019_05_04_135454
您现在可以更改注释并返回主页。此页面上还有两个工具栏项:第一个是“取消”按钮,允许您放弃所做的任何编辑。警报询问您是否确定。您也可以点击删除项目以删除注释,同时提供确认提醒。
该程序的一个棘手方面是“取消”按钮。假设你正在编辑一个音符,你会分心,最终程序终止。下次启动程序时,您应该返回编辑屏幕并查看编辑内容。如果您随后调用“取消”命令,则应放弃编辑。
这意味着当正在编辑注释时暂停应用程序时,注释必须以两种不同的形式保存:预编辑注释和带编辑的注释。只有当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中的页面类型的种类将在下一章中进行探讨。

目录
相关文章
|
7月前
|
小程序
【微信小程序】-- 页面导航 -- 编程式导航(二十三)
【微信小程序】-- 页面导航 -- 编程式导航(二十三)
|
7月前
|
小程序 API
【微信小程序】-- 页面导航 -- 声明式导航(二十二)
【微信小程序】-- 页面导航 -- 声明式导航(二十二)
|
7月前
|
前端开发
前端知识笔记(十三)———单全选框控制方法,炒鸡无敌方便!!!
前端知识笔记(十三)———单全选框控制方法,炒鸡无敌方便!!!
41 0
|
JavaScript 前端开发 Android开发
第二十四章:页面导航(十六)
保存和恢复导航堆栈 许多多页面应用程序的页面体系结构比DataTransfer6更复杂,您需要一种通用的方法来保存和恢复整个导航堆栈。此外,您可能希望将导航堆栈的保存与系统方式集成,以保存和恢复每个页面的状态,特别是如果您不使用MVVM。
499 0
|
XML JSON Android开发
第二十四章:页面导航(十五)
保存和恢复页面状态特别是当您开始使用多页面应用程序时,将应用程序的页面视为数据的主要存储库非常有用,而仅仅是作为底层数据的临时可视化和交互式视图。这里的关键词是暂时的。如果您在用户与之交互时保持基础数据是最新的,那么页面可以显示和消失而不必担心。
656 0
|
JavaScript Android开发
第二十四章:页面导航(十四)
切换到ViewModel此时应该很明显,Information类应该真正实现INotifyPropertyChanged。 在DataTransfer5中,Information类已成为InformationViewModel类。
606 0
|
JavaScript Android开发 索引
第二十四章:页面导航(十三)
App类中介在Xamarin.Forms应用程序中,在公共代码项目中执行的第一个代码是通常名为App的类的构造函数,该类派生自Application。 在程序终止之前,此App对象保持不变,并且程序中的任何代码都可以通过静态Application.Current属性使用它。
516 0
|
Android开发 索引
第二十四章:页面导航(十一)
消息中心您可能不喜欢两个页面类直接相互调用方法的想法。 它似乎适用于小样本,但对于具有大量类间通信的大型程序,您可能更喜欢一些不需要实际页面实例的更灵活的东西。这样的工具是Xamarin.Forms MessagingCenter类。
557 0
|
Android开发 索引
第二十四章:页面导航(十二)
事件在方法调用方法和消息中心通信方法中,信息页面需要知道主页的类型。 如果可以从不同类型的页面调用相同的信息页面,这有时是不合需要的。这个问题的一个解决方案是info类实现一个事件,这就是DataTransfer3中采用的方法。
515 0
|
JavaScript Android开发 索引
第二十四章:页面导航(十)
属性和方法调用调用PushAsync或PushModalAsync的页面显然可以直接访问它导航到的类,因此它可以设置属性或调用该页面对象中的方法以将信息传递给它。但是,调用PopAsync或PopModalAsync的页面还有一些工作要做,以确定它返回的页面。
555 0