author | ms.author | ms.date | ms.topic | no-loc | |||||
---|---|---|---|---|---|---|---|---|---|
jwmsft |
jimwalk |
03/26/2025 |
include |
|
This portion of the tutorial introduces the concepts of data views and models.
In the previous steps of the tutorial, you added a new page to the project that lets the user save, edit, or delete a single note. However, because the app needs to handle more than one note, you need to add another page that displays all the notes (call it AllNotesPage
). This page let's the user choose a note to open in the editor page so they can view, edit, or delete it. It should also let the user create a new note.
To accomplish this, AllNotesPage
needs to have a collection of notes, and a way to display the collection. This is where the app runs into trouble because the note data is tightly bound to the NotePage
file. In AllNotesPage
, you just want to display all the notes in a list or other collection view, with information about each note, like the date it was created and a preview of the text. With the note text being tightly bound to the TextBox
control, there's no way to do this.
Before you add a page to show all the notes, let's make some changes to separate the note data from the note presentation.
Typically, a WinUI app has at least a view layer and a data layer.
The view layer defines the UI using XAML markup. The markup includes data binding expressions (such as x:Bind) that define the connection between specific UI components and data members. Code-behind files are sometimes used as part of the view layer to contain additional code needed to customize or manipulate the UI, or to extract data from event handler arguments before calling a method that performs the work on the data.
The data layer, or model, defines the types that represent your app data and related logic. This layer is independent of the view layer, and you can create multiple different views that interact with the data.
Currently, the NotePage
represents a view of data (the note text). However, after the data is read into the app from the system file, it exists only in the Text
property of the TextBox
in NotePage
. It's not represented in the app in a way that lets you present the data in different ways or in different places; that is, the app doesn't have a data layer. You'll restructure the project now to create the data layer.
Tip
You can download or view the code for this tutorial from the GitHub repo. To see the code as it is in this step, see this commit: note page - view-model.
Refactor the existing code to separate the model from the view. The next few steps will organize the code so that views and models are defined separately from each other.
-
In Solution Explorer, right-click on the WinUINotes project and select Add > New Folder. Name the folder :::no-loc text="Models":::.
-
Right-click on the WinUINotes project again and select Add > New Folder. Name the folder :::no-loc text="Views":::.
-
Find the NotePage.xaml item and drag it to the :::no-loc text="Views"::: folder. The NotePage.xaml.cs file should move with it.
[!NOTE] When you move a file, Visual Studio usually prompts you with a warning about how the move operation may take a long time. This shouldn't be a problem here, press OK if you see this warning.
Visual Studio may also ask you if you want to adjust the namespace of the moved file. Select No. You'll change the namespace in the next steps.
Now that the view has been moved to the :::no-loc text="Views"::: folder, you'll need to update the namespaces to match. The namespace for the XAML and code-behind files of the pages is set to WinUINotes
. This needs to be updated to WinUINotes.Views
.
-
In the Solution Explorer pane, expand NotePage.xaml to reveal the code-behind file.
-
Double-click on the NotePage.xaml.cs item to open the code editor if it's not already open. Change the namespace to
WinUINotes.Views
:namespace WinUINotes.Views
-
Double-click on the NotePage.xaml item to open the XAML editor if it's not already open. The old namespace is referenced through the
x:Class
attribute, which defines which class type is the code-behind for the XAML. This entry isn't just the namespace, but the namespace with the type. Change thex:Class
value toWinUINotes.Views.NotePage
:x:Class="WinUINotes.Views.NotePage"
In the previous step, you created the note page and updated MainWindow.xaml
to navigate to it. Remember that it was mapped with the local:
namespace mapping. It's common practice to map the name local
to the root namespace of your project, and the Visual Studio project template already does this for you (xmlns:local="using:WinUINotes"
). Now that the page has moved to a new namespace, the type mapping in the XAML is now invalid.
Fortunately, you can add your own namespace mappings as needed. You need to do this to access items in different folders you create in your project. This new XAML namespace will map to the namespace of WinUINotes.Views
, so name it views
. The declaration should look like the following attribute: xmlns:views="using:WinUINotes.Views"
.
-
In the Solution Explorer pane, double-click on the MainWindow.xaml entry to open it in the XAML editor.
-
Add this new namespace mapping on the line below the mapping for
local
:xmlns:views="using:WinUINotes.Views"
-
The
local
XAML namespace was used to set theFrame.SourcePageType
property, so change it toviews
there. Your XAML should now look like this:<Window x:Class="WinUINotes.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:WinUINotes" xmlns:views="using:WinUINotes.Views" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" Title="WinUI Notes"> <!-- ... Unchanged XAML not shown. --> <Frame x:Name="rootFrame" Grid.Row="1" SourcePageType="views:NotePage"/> <!-- ... Unchanged XAML not shown. --> </Window>
-
Build and run the app. The app should run without any compiler errors, and everything should still work as before.
Currently, the model (the data) is embedded in the note view. You'll create a new class to represent a note page's data:
-
In the Solution Explorer pane, right-click on the :::no-loc text="Models"::: folder and select Add > Class....
-
Name the class Note.cs and press Add. The Note.cs file will open in the code editor.
-
Replace the code in the Note.cs file with this code, which makes the class
public
and adds properties and methods for handling a note:using System; using System.Threading.Tasks; using Windows.Storage; namespace WinUINotes.Models { public class Note { private StorageFolder storageFolder = ApplicationData.Current.LocalFolder; public string Filename { get; set; } = string.Empty; public string Text { get; set; } = string.Empty; public DateTime Date { get; set; } = DateTime.Now; public Note() { Filename = "notes" + DateTime.Now.ToBinary().ToString() + ".txt"; } public async Task SaveAsync() { // Save the note to a file. StorageFile noteFile = (StorageFile)await storageFolder.TryGetItemAsync(Filename); if (noteFile is null) { noteFile = await storageFolder.CreateFileAsync(Filename, CreationCollisionOption.ReplaceExisting); } await FileIO.WriteTextAsync(noteFile, Text); } public async Task DeleteAsync() { // Delete the note from the file system. StorageFile noteFile = (StorageFile)await storageFolder.TryGetItemAsync(Filename); if (noteFile is not null) { await noteFile.DeleteAsync(); } } } }
-
Save the file.
You'll notice that this code is very similar to the code in NotePage.xaml.cs, with a few changes and additions.
Filename
and Text
have been changed to public
properties, and a new Date
property has been added.
The code to save and delete the files has been placed in public
methods. It is mostly identical to the code you used in the button Click
event handlers in NotePage
, but extra code to update the view after the file is deleted has been removed. It's not needed here because you'll be using data binding to keep the model and view synchronized.
These async method signatures return Task instead of void
. The Task
class represents a single asynchronous operation that does not return a value. Unless the method signature requires void
, as is the case for the Click
event handlers, async
methods should return a Task
.
You also won't be keeping a reference to the StorageFile
that holds the note anymore. You just try to get the file when you need it to save or delete.
In NotePage
, you used a placeholder for the file name: note.txt
. Now that the app supports more than one note, file names for saved notes need to be different and unique. To do this, set the Filename
property in the constructor. You can use the DateTime.ToBinary method to create a part of the file name based on the current time and make the file names unique. The generated file name looks like this: notes-8584626598945870392.txt
.
Now you can update the NotePage
view to use the Note
data model and delete code that was moved to the Note
model.
-
Open the Views\NotePage.xaml.cs file if it's not already open in the editor.
-
After the last
using
statement at the top of the page, add a newusing
statement to give your code access to the classes in theModels
folder and namespace.using WinUINotes.Models;
-
Delete these lines from the class:
private StorageFolder storageFolder = ApplicationData.Current.LocalFolder; private StorageFile? noteFile = null; private string fileName = "note.txt";
-
Instead, add a
Note
object namednoteModel
in their place. This represents the note data thatNotePage
provides a view of.private Note? noteModel;
-
You also don't need the
NotePage_Loaded
event handler anymore. You won't be reading text directly from the text file into the TextBox. Instead, the note text will be read intoNote
objects. You'll add the code for this when you add theAllNotesPage
in a later step. Delete these lines.Loaded += NotePage_Loaded; ... private async void NotePage_Loaded(object sender, RoutedEventArgs e) { noteFile = (StorageFile)await storageFolder.TryGetItemAsync(fileName); if (noteFile is not null) { NoteEditor.Text = await FileIO.ReadTextAsync(noteFile); } }
-
Replace the code in the
SaveButton_Click
method with this:if (noteModel is not null) { await noteModel.SaveAsync(); }
-
Replace the code in the
DeleteButton_Click
method with this:if (noteModel is not null) { await noteModel.DeleteAsync(); }
Now you can update the XAML file to use the Note
model. Previously, you read the text directly from the text file into the TextBox.Text
property in the code-behind file. Now, you use data binding for the Text
property.
-
Open the Views\NotePage.xaml file if it's not already open in the editor.
-
Add a
Text
attribute to theTextBox
control. Bind it to theText
property ofnoteModel
:Text="{x:Bind noteModel.Text, Mode=TwoWay}"
. -
Update the
Header
to bind to theDate
property ofnoteModel
:Header="{x:Bind noteModel.Date.ToString()}"
.<TextBox x:Name="NoteEditor" <!-- ↓ Add this line. ↓ --> Text="{x:Bind noteModel.Text, Mode=TwoWay}" AcceptsReturn="True" TextWrapping="Wrap" PlaceholderText="Enter your note" <!-- ↓ Update this line. ↓ --> Header="{x:Bind noteModel.Date.ToString()}" ScrollViewer.VerticalScrollBarVisibility="Auto" Width="400" Grid.Column="1"/>
Data binding is a way for your app's UI to display data, and optionally to stay in sync with that data. The Mode=TwoWay
setting on the binding means that the TextBox.Text
and noteModel.Text
properties are automatically synchronized. When the text is updated in the TextBox
, the changes are reflected in the Text
property of the noteModel
, and if noteModel.Text
is changed, the updates are reflected in the TextBox
.
The Header
property uses the default Mode
of OneTime
because the noteModel.Date
property doesn't change after the file is created. This code also demonstrates a powerful feature of x:Bind
called function binding, which lets you use a function like ToString
as a step in the binding path.
Important
It's important to choose the correct BindingMode; otherwise, your data binding might not work as expected. (A common mistake with {x:Bind}
is to forget to change the default BindingMode
when OneWay
or TwoWay
is needed.)
Name | Description |
---|---|
OneTime |
Updates the target property only when the binding is created. Default for {x:Bind} . |
OneWay |
Updates the target property when the binding is created. Changes to the source object can also propagate to the target. Default for {Binding} . |
TwoWay |
Updates either the target or the source object when either changes. When the binding is created, the target property is updated from the source. |
Data binding supports the separation of your data and UI, and that results in a simpler conceptual model as well as better readability, testability, and maintainability of your app.
In WinUI, there are two kinds of binding you can choose from:
- The
{x:Bind}
markup extension is processed at compile-time. Some of its benefits are improved performance and compile-time validation of your binding expressions. It's recommended for binding in WinUI apps. - The
{Binding}
markup extension is processed at run-time and uses general-purpose runtime object inspection.
:::image type="icon" source="../media/doc-icon-sm.png" border="false"::: Learn more in the docs:
Model-View-ViewModel (MVVM) is a UI architectural design pattern for decoupling UI and non-UI code that is popular with .NET developers. You'll probably see and hear it mentioned as you learn more about creating WinUI apps. Separating the views and models, as you've done here, is the first step towards a full MVVM implementation of the app, but it's as far as you'll go in this tutorial.
Note
We've used the term "model" to refer to the data model in this tutorial, but it's important to note that this model is more closely aligned with the ViewModel in a full MVVM implementation, while also incorporating aspects of the Model.
To learn more about MVVM, see these resources: