author | ms.author | ms.date | ms.topic | no-loc | ||||||
---|---|---|---|---|---|---|---|---|---|---|
jwmsft |
jimwalk |
03/26/2025 |
include |
|
Now you'll create a page that allows a user to edit a note, and then you'll write the code to save or delete the note.
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 - initial.
First, add the new page to the project:
-
In the Solution Explorer pane of Visual Studio, right-click on the WinUINotes project > Add > New Item....
-
In the Add New Item dialog, select WinUI in the template list on the left-side of the window. Next, select the Blank Page (WinUI 3) template. Name the file NotePage.xaml, and then select Add.
-
The NotePage.xaml file will open in a new tab, displaying all of the XAML markup that represents the UI of the page. Replace the
<Grid> ... </Grid>
element in the XAML with the following markup:<Grid Padding="16" RowSpacing="8"> <Grid.RowDefinitions> <RowDefinition Height="*"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="400"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <TextBox x:Name="NoteEditor" AcceptsReturn="True" TextWrapping="Wrap" PlaceholderText="Enter your note" Header="New note" ScrollViewer.VerticalScrollBarVisibility="Auto" Width="400" Grid.Column="1"/> <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="4" Grid.Row="1" Grid.Column="1"> <Button Content="Save"/> <Button Content="Delete"/> </StackPanel> </Grid>
-
Save the file by pressing CTRL + S, clicking the Save icon in the tool bar, or by selecting the menu File > Save NotePage.xaml.
If you run the app right now, you won't see the note page you just created. That's because you still need to set it as the content of the
Frame
control inMainWindow
. -
Open MainWindow.xaml and set
NotePage
as the SourcePageType on theFrame
, like this:<Frame x:Name="rootFrame" Grid.Row="1" SourcePageType="local:NotePage"/>
Now when you run the app, the
Frame
will load an instance ofNotePage
and show it to the user.
Important
XAML namespace (xmlns) mappings are the XAML counterpart to the C# using
statement. local:
is a prefix that is mapped for you within the XAML pages for your app project (xmlns:local="using:WinUINotes"
). It's mapped to refer to the same namespace that's created to contain the x:Class
attribute and code for all the XAML files including App.xaml. As long as you define any custom classes you want to use in XAML in this same namespace, you can use the local:
prefix to refer to your custom types in XAML.
Let's break down the key parts of the XAML controls placed on the page:
:::image type="content" source="../media/note/app-layout.png" alt-text="The new note page UI with the grid highlighted by Visual Studio.":::
-
The Grid.RowDefinitions and Grid.ColumnDefinitions define a grid with 2 rows and 3 columns (placed below the title bar).
- The bottom row is automatically (
Auto
) sized to fit its content, the two buttons. The top row uses all the remaining vertical space (*
). - The middle column is
400
epx wide and is where the note editor goes. The columns on either side are empty and split all the remaining horizontal space between them (*
).
[!NOTE] Because of how the scaling system works, when you design your XAML app, you're designing in effective pixels, not actual physical pixels. Effective pixels (epx) are a virtual unit of measurement, and they're used to express layout dimensions and spacing, independent of screen density.
- The bottom row is automatically (
-
<TextBox x:Name="NoteEditor" ... > ... </TextBox>
is a text entry control (TextBox) configured for multi-line text entry, and is placed in the top center cell of theGrid
(Grid.Column="1"
). Row and column indexes are 0-based, and by default, controls are placed in row 0 and column 0 of the parentGrid
. So this is the equivalent of specifying Row 0, Column 1. -
<StackPanel Orientation="Horizontal" ... > ... </StackPanel>
defines a layout control (StackPanel) that stacks its children either vertically (default) or horizontally. It's placed in the bottom center cell of theGrid
(Grid.Row="1" Grid.Column="1"
).[!NOTE]
Grid.Row="1" Grid.Column="1"
is an example of XAML attached properties. Attached properties let one XAML object set a property that belongs to a different XAML object. Often, as in this case, child elements can use attached properties to inform their parent element of how they are to be presented in the UI. -
Two
<Button>
controls are inside the<StackPanel>
and arranged horizontally. You'll add the code to handle the buttons' Click events in the next section.
:::image type="icon" source="../media/doc-icon-sm.png" border="false"::: Learn more in the docs:
Open the NotePage.xaml.cs code-behind file. When you add a new XAML file, the code-behind contains a single line in the constructor, a call to the InitializeComponent
method:
namespace WinUINotes
{
public sealed partial class NotePage : Page
{
public NotePage()
{
this.InitializeComponent();
}
}
}
The InitializeComponent
method reads the XAML markup and initializes all of the objects defined by the markup. The objects are connected in their parent-child relationships, and the event handlers defined in code are attached to events set in the XAML.
Now you're going to add code to the NotePage.xaml.cs code-behind file to handle loading and saving notes.
-
Add the following variable declarations to the
NotePage
class:public sealed partial class NotePage : Page { private StorageFolder storageFolder = ApplicationData.Current.LocalFolder; private StorageFile? noteFile = null; private string fileName = "note.txt";
When a note is saved, it's saved to the app's local storage as a text file.
You use the StorageFolder class to access the app's local data folder. This folder is specific to your app, so notes saved here can't be accessed by other apps. You use the StorageFile class to access the text file saved in this folder. The name of the file is represented by the
fileName
variable. For now, setfileName
to "note.txt". -
Create an event handler for the note page's Loaded event.
public NotePage() { this.InitializeComponent(); // ↓ Add this. ↓ Loaded += NotePage_Loaded; } // ↓ Add this event handler method. ↓ 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); } }
In this method, you call TryGetItemAsync to retrieve the text file from the folder. If the file doesn't exist, it returns
null
. If the file does exist, call ReadTextAsync to read the text from the file into theNoteEditor
control's Text property. (Remember,NoteEditor
is theTextBox
control you created in the XAML file. You reference it here in your code-behind file using thex:Name
you assigned to it.)[!IMPORTANT] You need to mark this method with the
async
keyword because the file access calls are asynchronous. In short, if you call a method that ends in...Async
(likeTryGetItemAsync
), you can add the await operator to the call. This keeps subsequent code from executing until the awaited call completes and keeps your UI responsive. When you useawait
, the method that you're calling from needs to be marked with the async keyword. For more info, see Call asynchronous APIs in C#.
:::image type="icon" source="../media/doc-icon-sm.png" border="false"::: Learn more in the docs:
Next, add the Click event handlers for the for the Save and Delete buttons. Adding event handlers is something that you'll do often while creating your apps, so Visual Studio provides several features to make it easier.
-
In the NotePage.xaml file, place your cursor after the
Content
attribute in the SaveButton
control. TypeClick=
. At this point, Visual Studio should pop up an auto-complete UI that looks like this::::image type="content" source="../media/note/new-event-xaml.png" alt-text="A screenshot of the Visual Studio new event handler auto complete UI in the XAML editor":::
- Press the down-arrow key to select <New Event Handler>, then press Tab. Visual Studio will complete the attribute with
Click="Button_Click"
and add an event handler method namedButton_Click
in the NotePage.xaml.cs code-behind file.
Now, you should rename the
Button_Click
method to something more meaningful. You'll do that in the following steps. - Press the down-arrow key to select <New Event Handler>, then press Tab. Visual Studio will complete the attribute with
-
In NotePage.xaml.cs, find the method that was added for you:
private void Button_Click(object sender, RoutedEventArgs e) { }
[!TIP] To locate code in your app, click Search in the Visual Studio title bar and use the Code Search option. Double-click the search result to open the code in the code editor.
:::image type="content" source="../media/note/vs-code-search.png" alt-text="Search feature in Visual Studio":::
-
Place your cursor before the "B" in
Button
and typeSave
. Wait a moment, and the method name will be highlighted in green. -
When you hover over the method name, Visual Studio will show a tooltip with a screwdriver or lightbulb icon. Click the down-arrow button next to the icon, then click Rename 'Button_Click' to 'SaveButton_Click'.
:::image type="content" source="../media/note/method-rename.png" alt-text="The Visual Studio method rename popup UI.":::
Visual Studio will rename the method everywhere in your app, including in the XAML file where you first added it to the
Button
. -
Repeat these steps for the Delete button, and rename the method to
DeleteButton_Click
.
Now that the event handlers are hooked up, you can add the code to save and delete the note file.
-
Add this code in the
SaveButton_Click
method to save the file. Notice that you also need to update the method signature with theasync
keyword.private async void SaveButton_Click(object sender, RoutedEventArgs e) { if (noteFile is null) { noteFile = await storageFolder.CreateFileAsync(fileName, CreationCollisionOption.ReplaceExisting); } await FileIO.WriteTextAsync(noteFile, NoteEditor.Text); }
In the
SaveButton_Click
method, you first check to see ifnoteFile
has been created. If it'snull
, then you have to create a new file in the local storage folder with the name represented by thefileName
variable, and assign the file to thenoteFile
variable. Then, you write the text in theTextBox
control to the file represented bynoteFile
. -
Add this code in the
DeleteButton_Click
method to delete the file. You need to update the method signature with theasync
keyword here, too.private async void DeleteButton_Click(object sender, RoutedEventArgs e) { if (noteFile is not null) { await noteFile.DeleteAsync(); noteFile = null; NoteEditor.Text = string.Empty; } }
In the
DeleteButton_Click
method, you first check to see ifnoteFile
exists. If it does, delete the file represented bynoteFile
from the local storage folder and setnoteFile
tonull
. Then, reset the text in theTextBox
control to an empty string.[!IMPORTANT] After the text file is deleted from the file system, it's important to set
noteFile
tonull
. Remember thatnoteFile
is a StorageFile that provides access to the system file in your app. After the system file is deleted,noteFile
still points to where the system file was, but doesn't know that it no longer exists. If you try to read, write, or delete the system file now, you'll get an error. -
Save the file by pressing CTRL + S, clicking the Save icon in the tool bar, or by selecting the menu File > Save NotePage.xaml.cs.
The final code for the code-behind file should look like this:
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System;
using Windows.Storage;
namespace WinUINotes
{
public sealed partial class NotePage : Page
{
private StorageFolder storageFolder = ApplicationData.Current.LocalFolder;
private StorageFile? noteFile = null;
private string fileName = "note.txt";
public NotePage()
{
this.InitializeComponent();
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);
}
}
private async void SaveButton_Click(object sender, RoutedEventArgs e)
{
if (noteFile is null)
{
noteFile = await storageFolder.CreateFileAsync(fileName, CreationCollisionOption.ReplaceExisting);
}
await FileIO.WriteTextAsync(noteFile, NoteEditor.Text);
}
private async void DeleteButton_Click(object sender, RoutedEventArgs e)
{
if (noteFile is not null)
{
await noteFile.DeleteAsync();
noteFile = null;
NoteEditor.Text = string.Empty;
}
}
}
}
With this code in place, you can test the app to make sure the note saves and loads correctly.
- Build and run the project by pressing F5, clicking the Debug "Start" button in the tool bar, or by selecting the menu Run > Start Debugging.
- Type into the text entry box and press the Save button.
- Close the app, then restart it. The note you entered should be loaded from the device's storage.
- Press the Delete button.
- Close the app, restart it. You should be presented with a new blank note.
Important
After you've confirmed that saving and deleting a note works correctly, create and save a new note again. You'll want to have a saved note to test the app in later steps.