一个简单的自动保存/恢复画图应用程序






4.93/5 (9投票s)
一个解释自动保存功能基本实现的应用程序
引言
当您花费很长时间在一个应用程序上,然后应用程序崩溃时,您会有多么恼火?您忘记了进行“文件 -> 保存”或“Ctrl + S”,导致数据完全丢失。您所付出的努力都白费了。也许下次,您会更加谨慎,并定期按下“Ctrl + S”。也许您会遵循几次,然后又忘了这样做。
应用程序崩溃可能发生的原因有很多,例如停电、应用程序中的错误导致应用程序崩溃或恶意病毒。您可以防范停电或病毒,但直到获得补丁/新版本之前,您无法应对错误。
背景
有些应用程序具有自动保存功能,可以在一定时间间隔内保存工作。如果应用程序崩溃,用户会被提示加载自动保存的工作。Microsoft Word 内置了此功能,可以按如下方式进行配置。可以通过左上角的 Office 按钮 -> Word 选项 -> 保存选项卡进行访问。
另一个例子是 Microsoft Outlook 和 Gmail 在一定时间间隔后保存电子邮件。
我将编写一个简单的画图应用程序,它可以在一定时间间隔内保存设计。它使用 WPF 和 C# 开发。该应用程序只是为了理解自动保存的概念,不包含 Microsoft Word 中的高级功能。但是,它可以扩展以创建这样的功能。我会尽量保持简单。
自动保存画图应用程序
应用程序功能列表如下
- 新建 – 清除设计
- 保存 – 将工作设计保存到指定位置
- 打开 – 打开选定的 Ink 序列化格式 (*.ink) 文件
- 退出 – 退出应用程序
- 自动保存 – 在后台以指定的时间间隔将设计保存到隐藏位置
- 恢复 – 应用程序崩溃后重新启动应用程序时,在启动时加载最后一次自动保存的文件
让我们开始创建应用程序。
代码
- 打开 Visual Studio -> 新建项目 -> 其他项目类型 -> Visual Studio 解决方案 -> 空解决方案。将其命名为“AutosaveAndRecovery”。
- 添加一个新的 WPF 项目“AutosavePaint”。
- 用下面的 XAML 和 .cs 文件替换 MainWindow.xaml 和 MainWindow.xaml.cs。
MainWindow.xaml
<Window x:Class="AutosavePaint.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Menu HorizontalAlignment="Left" Height="38" VerticalAlignment="Top"
                                         Width="517" RenderTransformOrigin="0.32,-0.079">
            <MenuItem Header="File">
                <MenuItem Command="ApplicationCommands.New">
                    <MenuItem.CommandBindings>
                        <CommandBinding Command="ApplicationCommands.New"
                                        Executed="New"
                                        CanExecute="CanNew"/>
                    </MenuItem.CommandBindings>
                </MenuItem>
                <MenuItem Header="Open" Command="ApplicationCommands.Open"
                          HorizontalAlignment="Left" Width="157.507"
                          RenderTransformOrigin="0.502,0.502" Height="26" Margin="0,0,-13.17,0">
                    <MenuItem.CommandBindings>
                        <CommandBinding Command="ApplicationCommands.Open"
                                        Executed="Open"
                                        CanExecute="CanOpen"/>
                    </MenuItem.CommandBindings>
                </MenuItem>
                <MenuItem Header="Save" Margin="0,0,12,0" Command="ApplicationCommands.Save"
                           Height="25" RenderTransformOrigin="0.552,0.482">
                    <MenuItem.CommandBindings>
                        <CommandBinding Command="ApplicationCommands.Save"
                                        Executed="Save"
                                        CanExecute="CanSave"/>
                    </MenuItem.CommandBindings>
                </MenuItem>
                <MenuItem Header="Exit" Command="ApplicationCommands.Close">
                    <MenuItem.CommandBindings>
                        <CommandBinding Command="ApplicationCommands.Close"
                                        Executed="Exit"
                                        CanExecute="CanExit"/>
                    </MenuItem.CommandBindings>
                </MenuItem>
            </MenuItem>
        </Menu>
        <InkCanvas Name="inkCanvas" HorizontalAlignment="Left" Height="229"
             Margin="10,43,0,0" VerticalAlignment="Top" Width="497" Background="#FF9C9898"/>
        <StatusBar x:Name="statusBar" Height="33" Margin="10,277,10,0"
            VerticalAlignment="Top" Background="#FFD4CFCF">
            <StatusBarItem x:Name="statusBarItem" Content="StatusBarItem"
                 Height="33" VerticalAlignment="Top"/>
        </StatusBar>
    </Grid>
</Window>
MainWindow.xaml.cs
我们已经构建了应用程序的结构,但没有功能。运行应用程序并确保它能正常启动。我使用了 InkCanvas 进行绘图。将鼠标移动到中间的灰色区域。

现在,我们将逐一添加上述功能。
1. 新建:清除设计
添加以下代码以清除画布,在按下“新建”按钮时,设计将被清除。
/// <summary>
/// Clears the design
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void New(object sender, ExecutedRoutedEventArgs e)
{
     inkCanvas.Strokes.Clear();
     statusBarItem.Content = "Ready";
}
2. 保存:将工作设计保存到指定位置
添加以下代码将设计保存到文件。
        /// <summary>
        /// Saves currently working design
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        public void Save(object sender, ExecutedRoutedEventArgs e)
        {
            var saveFileDialog = new SaveFileDialog();
            saveFileDialog.Filter = "Ink Serialized Format (*.isf)|*.isf|";
            if (saveFileDialog.ShowDialog() == true)
            {
                Save(saveFileDialog.FileName);
            }
        }
        /// <summary>
        /// Saves the design to the specified file
        /// </summary>
        /// <param name="fileName">File to be saved</param>
        /// <returns>true, if saving is successful</returns>
        private bool Save(string fileName)
        {
            FileStream fs = null;
            try
            {
                fs = File.Open(fileName, FileMode.Create);
                inkCanvas.Strokes.Save(fs);
            }
            catch (Exception ex)
            {
                throw ex;
            }
            finally
            {
                if (fs != null)
                    fs.Close();
            }
            return true;
        }
设计将以 .ink* 扩展名的文件保存。设计也可以保存为位图格式。为了保持代码简洁,我没有包含它,本文的主要目的是演示自动保存和恢复。 这是一篇精彩的文章,由 Sacha Barber 撰写,关于使用 InkCanvas 的 Paint 应用程序。
3. 打开:打开选定的 Ink 序列化格式 (*.ink) 文件
添加以下代码以打开 *.ink 文件并解析引用。
        /// <summary>
        /// Opens a selected design
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        public void Open(object sender, ExecutedRoutedEventArgs e)
        {
            OpenFileDialog openFileDialog = new OpenFileDialog();
            openFileDialog.CheckFileExists = true;
            openFileDialog.Filter = "Ink Serialized Format (*.isf)|*.isf|" +
                         "All files (*.*)|*.*";
            if (openFileDialog.ShowDialog(this) == true)
            {
                string fileName = openFileDialog.FileName;
                if (!fileName.ToLower().EndsWith(".isf"))
                {
                    MessageBox.Show("The requested file is not a Ink Serialized Format
                                     file\r\n\r\nplease retry", Title);
                }
                else
                {
                    if (Open(fileName))
                    {
                        statusBarItem.Content = "Loaded";
                    }
                    else
                    {
                        statusBarItem.Content = "An error occured while opening the file";
                    }
                }
            }
        }
        /// <summary>
        /// Opens the specified file in canvas
        /// </summary>
        /// <param name="fileName">File to be opened</param>
        /// <returns>true, if opening is successful</returns>
        private bool Open(string fileName)
        {
            FileStream fs = null;
            try
            {
                this.inkCanvas.Strokes.Clear();
                fs = new FileStream(fileName, FileMode.Open);
                inkCanvas.Strokes = new System.Windows.Ink.StrokeCollection(fs);
            }
            catch (Exception)
            {
                return false;
            }
            finally
            {
                if (fs != null)
                    fs.Close();
            }
            return true;
        }
4. 退出:退出应用程序
添加以下代码以退出应用程序
        /// <summary>
        /// Exits the application
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        public void Exit(object sender, ExecutedRoutedEventArgs e)
        {
            this.Close();
        }
5. 自动保存:在后台以指定的时间间隔将设计保存到隐藏位置
为了实现自动保存,我们需要考虑以下几点。
- 位置 – 自动保存文件的位置。我们将对其隐藏用户。
- 后台操作 - 保存操作必须在后台的工作线程中执行,以使应用程序保持响应。
- 自动保存间隔 – 这决定了两次连续保存操作之间的时间间隔。
首先定义位置。我使用了执行程序集的位置,并在其中创建了 Autosave 目录。此外,该目录需要被隐藏。添加了两个辅助方法 MakeDirectoryHidden(…) 和 MakeFileHidden(…)。有关实现,请参阅下载的代码。
        private readonly string _backupFilePath;
        private string _backupFile;
        private const string BACKUP_FILE_NAME = "PaintAppBackup.bk";
        /// <summary>
        /// Constructor to initialize this class
        /// </summary>
        public MainWindow()
        {
            InitializeComponent();
            statusBarItem.Content = "Ready";
            _backupFilePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) +
                              "\\" + BACKUP_DIRECTORY;
            _backupFile = _backupFilePath + "\\" + BACKUP_FILE_NAME;
            if (!Directory.Exists(_backupFilePath))
            {
                Directory.CreateDirectory(_backupFilePath);
                MakeDirectoryHidden(_backupFilePath);
            }
        }
运行应用程序,如果在调试模式下运行应用程序,您会发现在 ..\\ AutosaveAndRecovery\AutosavePaint\bin\Debug\Autosave 目录下创建了 Autosave 目录。
为了在后台保存设计,我使用了 BackgroundWorker。
已添加两个方法来实现此功能。
        /// <summary>
        /// This is called when saving of design asynchronously is completed
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void OnSaveAsyncCompletion(object sender, RunWorkerCompletedEventArgs e)
        {
            // First, handle the case where an exception was thrown.
            if (e.Error != null)
            {
                statusBarItem.Content = "An error occured while saving the design";
                DeleteBackupFile();
            }
            else
            {
                // Finally, handle the case where the operation
                // succeeded.
                statusBarItem.Content = "Saved";
                MakeFileHidden(_backupFile);
            }
        }
现在,最后一件事情是以一定的间隔定期调用 SaveAync(…)。我选择了 20 秒作为保存间隔。为了定期调用它,我使用了 DispatcherTimer。DispatcherTimer 定期调用以下方法。
        /// <summary>
        /// This method is periodically called at a specified interval
        /// </summary>
        /// <param name="o"></param>
        /// <param name="e"></param>
        void PeriodicSave(object o, EventArgs e)
        {
            statusBarItem.Content = "Saving";
            if (!Directory.Exists(_backupFilePath))
            {
                Directory.CreateDirectory(_backupFilePath);
                MakeDirectoryHidden(_backupFilePath);
            }
            DeleteBackupFile();
            SaveAsync(_backupFile);
        }
为了启动/停止 Dispatcher,已添加窗口的 Loaded 和 Closing 事件。
        /// <summary>
        /// Called when window is loaded
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            _dispatcherTimer = new DispatcherTimer();
            _dispatcherTimer.Interval = TimeSpan.FromMilliseconds(AUTOSAVE_INTERVAL_SECONDS * 1000);
            _dispatcherTimer.Tick += PeriodicSave;
            _dispatcherTimer.Start();
            statusBarItem.Content = "Ready";
        }
        /// <summary>
        /// Called when window is closing
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Window_Closing(object sender, CancelEventArgs e)
        {
            if (_dispatcherTimer != null)
            {
                _dispatcherTimer.Stop();
                _dispatcherTimer = null;
            }
        }
运行应用程序,您会注意到在 20 秒后保存设计时发生了错误。引发的异常是跨线程异常。我们正在尝试从后台线程访问 UI 组件 (InkCanvas)。要克服这个问题,请像下面这样将 UI 访问调用封送回 UI 线程。
        /// <summary>
        /// Saves the design to the specified file
        /// </summary>
        /// <param name="fileName">File to be saved</param>
        /// <returns>true, if saving is successful</returns>
        private bool Save(string fileName)
        {
            FileStream fs = null;
            try
            {
                fs = File.Open(fileName, FileMode.Create);
                ExecuteOnUIThread(() => inkCanvas.Strokes.Save(fs)); <<ç=
            }
            catch (Exception ex)
            {
                throw ex;
            }
            finally
            {
                if (fs != null)
                    fs.Close();
            }
            return true;
        }
        /// <summary>
        /// Marshall the call to the UI thread
        /// </summary>
        /// <param name="action"></param>
        private void ExecuteOnUIThread(Action action)
        {
            var dispatcher = Application.Current.Dispatcher;
            if (dispatcher != null)
            {
                dispatcher.Invoke(action);
            }
            else
            {
                action();
            }
        }
运行应用程序,进行一些草图,20 秒后,文件将被保存。
6. 恢复:在应用程序崩溃后重新启动应用程序时,加载最后一次自动保存的文件
这是最后一个功能,如果我们无法恢复最后一次自动保存的文件,自动保存就没有意义了。下面的流程图描绘了恢复机制。

CheckAndLoadBackupFile(…) 方法负责恢复。它在应用程序启动后立即在 loaded 事件中被调用。此外,DeleteBackupFile(…) 在应用程序的 closing 事件中被调用。
设计自动保存后的应用程序。

结论
自动保存和恢复是在应用程序中让用户生活更舒适的绝佳功能。我只是介绍了如何实现它。它可以扩展为高级功能。它可以用于将设计、草图或表单数据保存到文件/数据库。任何提供打开/保存选项的应用程序都是此类应用程序的理想选择。
欢迎评论和建议!
参考文献
- https://codeproject.org.cn/Articles/324/Autosave-and-Crash-Recovery
- https://codeproject.org.cn/Articles/4574/Save-and-Restore-User-Preferences?q=autosave
- https://codeproject.org.cn/Articles/18166/Scratchpad-An-Auto-Save-Notepad
- https://codeproject.org.cn/Articles/19102/Adventures-into-Ink-API-using-WPF
- https://codeproject.org.cn/Articles/16579/Saving-Rebuilding-InkCanvas-Strokes
- https://codeproject.org.cn/Articles/617868/Scribble-WPF-InkCanvas-Application-Using-PRISM-MVV



