I have a college assignment to create an app similar to Notepad using WPF. For this, I created in MainWindow.xaml a TabControl, each tab having a header (TextBlock) and the text content (TextBox). The TabControl is bound to an ObservableCollection of TabModel (the model class I used for tab items) named "Tabs".
The problem arises when the "Save As" function is called. It's supposed to save the tab content in a .txt file (this works just fine) and rename the active tab header to match the name of the saved .txt file (it doesn't update the header). Debugging shows that the data inside "Tabs" is updated correctly, but the UI doesn't reflect that.
This is the code inside MainWindow.xaml:
<Window x:Class="Notepad__.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Notepad__.Commands"
mc:Ignorable="d"
Title="Notepad--"
Name="WindowHeader"
Height="1000"
Width="1200"
WindowStartupLocation="CenterScreen"
Icon="Images\icon.png">
<Window.DataContext>
<local:FileCommands/>
</Window.DataContext>
<Grid>
<Grid.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#E7E9BB" Offset="0.0" />
<GradientStop Color="#403B4A" Offset="1.0" />
</LinearGradientBrush>
</Grid.Background>
<Grid.RowDefinitions>
<RowDefinition Height="25"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Menu>
<Menu.ItemsPanel>
<ItemsPanelTemplate>
<DockPanel HorizontalAlignment="Left"/>
</ItemsPanelTemplate>
</Menu.ItemsPanel>
<MenuItem Header="File" FontSize="14">
<MenuItem Header="New" Command="{Binding Path=CommandNew}"/>
<MenuItem Header="Open..." Command="{Binding Path=CommandOpen}"/>
<MenuItem Header="Save"/>
<MenuItem Header="Save as..." Command="{Binding Path=CommandSaveAs}"/>
<Separator/>
<MenuItem Header="Exit" Command="{Binding Path=CommandExit}"/>
</MenuItem>
<MenuItem Header="Search" FontSize="14">
<MenuItem Header="Find..."/>
<MenuItem Header="Replace..."/>
<MenuItem Header="Replace All..."/>
</MenuItem>
<MenuItem Header="Help" FontSize="14">
<MenuItem Header="About"/>
</MenuItem>
</Menu>
<TabControl
Name = "TabsControl"
ItemsSource="{Binding Tabs, UpdateSourceTrigger=PropertyChanged}"
SelectedIndex="{Binding CurrentTab, UpdateSourceTrigger=PropertyChanged}"
Grid.Row="1"
Margin="25"
FontSize="14">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock
Text="{Binding Filename, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<TextBox
Text="{Binding Content, UpdateSourceTrigger=PropertyChanged}"
TextWrapping="Wrap"
AcceptsReturn="True"
VerticalScrollBarVisibility="Visible"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Grid.Row="1"
Margin="10"
FontSize="26"/>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Grid>
</Window>
This is the code inside FileCommands.cs, where I did the binding and the implementation of the SaveAs function. I excluded the "using"s:
namespace Notepad__.Commands
{
class FileCommands : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<TabModel> Tabs { get; set; }
private int currentTab;
public int CurrentTab { get => currentTab; set => SetProperty(ref currentTab, value); }
private ICommand m_new;
private ICommand m_open;
private ICommand m_saveAs;
public FileCommands()
{
Tabs = new ObservableCollection<TabModel>();
Tabs.Add(new TabModel { Filename = "File 1", Content = "" });
}
private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public void New(object parameter)
{
Tabs.Add(new TabModel { Filename = "File " (Tabs.Count 1), Content = "" });
}
public void Open(object parameter)
{
var openFileDialog = new OpenFileDialog
{
Title = "Select a text file...",
Filter = "Text files (*.txt)|*.txt|All files (*.*)|*.*"
};
if (openFileDialog.ShowDialog() == true)
{
Stream fileStream = openFileDialog.OpenFile();
using (StreamReader reader = new StreamReader(fileStream))
{
Tabs.Add(new TabModel { Filename = openFileDialog.SafeFileName, Content = reader.ReadToEnd() });
OnPropertyChanged("Tabs");
}
}
}
public void SaveAs(object parameter)
{
var saveFileDialog = new SaveFileDialog
{
Title = "Save...",
Filter = "Text files (*.txt)|*.txt|All files (*.*)|*.*"
};
if (saveFileDialog.ShowDialog() == true)
{
File.WriteAllText(saveFileDialog.FileName, Tabs[currentTab].Content);
Tabs[currentTab].Filename = saveFileDialog.SafeFileName;
}
OnPropertyChanged("Tabs");
}
public ICommand CommandNew
{
get
{
if (m_new == null)
m_new = new RelayCommand(New);
return m_new;
}
}
public ICommand CommandOpen
{
get
{
if (m_open == null)
m_open = new RelayCommand(Open);
return m_open;
}
}
public ICommand CommandSaveAs
{
get
{
if (m_saveAs == null)
m_saveAs = new RelayCommand(SaveAs);
return m_saveAs;
}
}
protected bool SetProperty<T>(ref T field, T newValue, [CallerMemberName] string propertyName = null)
{
if (!Equals(field, newValue))
{
field = newValue;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
return true;
}
return false;
}
}
}
I've also included the code for the functions "New" and "Open" as well. That's because they work correctly, adding a new tab using Tabs.Add works as intended and the UI is updated accordingly. This is what confuses me: Why does adding a tab work and updating a tab doesn't?
Sorry if this question is trivial. This is my first time working in C# and my first WPF project. Neither my classmates nor my lab teacher could help me solve my issue. Thanks for reading, if there's anything else from my code that I should include, please let me know :)
CodePudding user response:
As discussed in the comments, the TabModel
needs to implement the INotifyPropertyChanged
interface in order for the UI to be updated every time a property value is changed.
You can create a base class just for raising the notification.
See an example below:
The base:
internal abstract class ObservableBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected bool SetProperty<T>(ref T field, T newValue, [CallerMemberName] string propertyName = null)
{
if (Equals(field, newValue))
return false;
field = newValue;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
return true;
}
}
The model, derived from the base:
internal class TabModel : ObservableBase
{
private string _name;
private string _content;
public string Filename
{
get => _name;
set => SetProperty(ref _name, value);
}
public string Content
{
get => _content;
set => SetProperty(ref _content, value);
}
}
The same for the VM:
class FileCommands : ObservableBase
{
// ...
}