My WPF app is working in a strange way for me - some binding works, other not.
I have following situation:
A textbox - user provides an ID. Based on this ID an object is loaded or created. Some other properties are updated by values coming from the loaded/new object.
Binding for the ID textbox works fine. However, two other views (any other) not.
My code samples:
XAML:
<StackPanel Orientation="Horizontal" Margin="0,5,0,0">
<TextBlock Text="ID" FontFamily="Segoe UI Light" />
<TextBox x:Name="TB_PacientID" Width="100px" HorizontalAlignment="Left" Margin="5,0,0,0" Text="{Binding Path=PacientID}"/>
<TextBlock x:Name="TBL_NovyPacient" Text="novĂ˝ pacient" Margin="5,0,0,0" Foreground="Green" FontWeight="Bold" Visibility="{Binding Path=IsNewPacient,UpdateSourceTrigger=PropertyChanged,Converter={StaticResource BTVConverter}}"/>
</StackPanel>
<WrapPanel x:Name="WP_PacientData" Margin="-2,5,2,5" Visibility="{Binding PacientLoaded,Converter={StaticResource BTVConverter}}">
...
Viewmodel:
public int? PacientID
{
get
{
if (CurrentPacient == null)
return null;
return CurrentPacient.id;
}
set
{
if (value != null)
{
_pacient = App.instance.sessionData.serviceProxy.pacientById(value.Value);
if (_pacient == null)
{
CurrentPacient = new Pacient() { id = value.Value };
IsNewPacient = true;
}
else
{
CurrentPacient = _pacient;
}
OnPropertyChanged();
PacientLoaded = true;
}
}
}
// ...
public bool IsNewPacient
{
get{ return _isNewPacient; }
set
{
_isNewPacient = value;
OnPropertyChanged();
}
}
//...
public bool PacientLoaded
{
get{ return _pacientLoaded; }
set
{
_pacientLoaded = value;
OnPropertyChanged();
}
}
The idea:
User inputs the ID, an object is loaded or created and the WrapPanel is shown. If the object is newly created the TextBlock is shown as well.
The converters are working fine (tested in another window).
When the window loads, the binding is established well (if I set some fake values in ctor). When changing the ID in textbox, nothing other updates - except for the ID itself - the setter is fired well and the new value is read after OnPropertyChanged is called.
I hope I'm missing something very easy and stupid.
-Edit:
Current state:
TB_PacientID is working (updading), TBL_NovyPacient and WP_PacientData not working (updating).
I want:
All thee views updating from viewmodel (the code properties).
-Edit 2
I created a very simple example of my problem from scratch:
A window - two textboxes:
<Window x:Class="bindingTest.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">
<StackPanel>
<TextBox x:Name="TestTextBox" Text="{Binding ID, Mode=TwoWay}"/>
<TextBox x:Name="SecondTextBox" Text="{Binding IsNew, Mode=TwoWay}"/>
</StackPanel>
</Window>
Codebehind:
namespace bindingTest
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new TestViewModel();
}
}
}
And the viewmodel class:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace bindingTest
{
public abstract class ViewModelBase
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
public class TestViewModel : ViewModelBase
{
private bool _attOne;
private int? id;
private bool _isNew;
public bool IsNew
{
get
{
return _isNew;
}
set
{
_isNew = value;
OnPropertyChanged();
}
}
public int? ID
{
get
{
return id;
}
set
{
this.id = value;
IsNew = true;
OnPropertyChanged();
}
}
}
}
And what I simply want - If I change the number in the first textbox I want to have True in the second textbox automatically.
Yes, I am stupid.
My ViewModel base class lost the INotifyPropertyChanged interface while merging from another project.
So I called the OnPropertyChanged, but it has been my own OnPropertyChanged instead of implementation of the interface which is WPF binding waiting for.
I had threethings to point in your code sample:
You should use a TwoWay binding for setting the ID.
Are you sure the _pacient = App.instance.sessionData.serviceProxy.pacientById(value.Value); code returns always the same object instance.
Are you correctly using the INotifyPropertyChanged interface in most cases you raising a property change events looks like this: RaisePropertyChanged('PropertyName'); you are invoking: 'OnPropertyChanged();'
Hope this helps...
Related
I am new to MVVM and Avalonia and learn it by watching a course on YouTube to create an explorer app. So far I created a super simple explorer like this:
Here is the MainWindow.axaml:
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:AvaloniaMVVMExplorer.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:i="clr-namespace:Avalonia.Xaml.Interactivity;assembly=Avalonia.Xaml.Interactivity"
xmlns:ia="clr-namespace:Avalonia.Xaml.Interactions.Core;assembly=Avalonia.Xaml.Interactions"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="AvaloniaMVVMExplorer.Views.MainWindow"
Icon="/Assets/avalonia-logo.ico"
Title="AvaloniaMVVMExplorer"
WindowState="Maximized"
WindowStartupLocation="CenterScreen">
<Design.DataContext>
<!-- This only sets the DataContext for the previewer in an IDE,
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
<vm:MainWindowViewModel/>
</Design.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBox Grid.Row="0"
Text="{Binding CurrentFilePath}"/>
<ListBox Grid.Row="1"
x:Name="PathsLB"
Items="{Binding DirectoriesAndFiles}"
SelectedItem="{Binding SelectedFileEntity}">
<i:Interaction.Behaviors>
<ia:EventTriggerBehavior EventName="DoubleTapped">
<ia:InvokeCommandAction Command="{Binding OpenCommand}"
CommandParameter="{Binding ElementName=PathsLB, Path=SelectedItem}"/>
</ia:EventTriggerBehavior>
</i:Interaction.Behaviors>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid >
<TextBlock Text="{Binding Name}"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>
And here is MainWIndowViewModel.cs:
using ReactiveUI;
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Runtime.CompilerServices;
using System.Windows.Input;
using AvaloniaMVVMExplorer.ViewModels.Base;
using AvaloniaMVVMExplorer.ViewModels.Commands;
using AvaloniaMVVMExplorer.ViewModels.FileViewModels;
using AvaloniaMVVMExplorer.ViewModels.FileViewModels.Base;
namespace AvaloniaMVVMExplorer.ViewModels
{
internal class MainWindowViewModel : ViewModelBase
{
#region Properties
private string? currentFilePath;
public string? CurrentFilePath
{
get { return currentFilePath; }
set { currentFilePath = value; OnPropertyChanged(); }
}
private ObservableCollection<FileEntityViewModel>? directoriesAndFiles;
public ObservableCollection<FileEntityViewModel>? DirectoriesAndFiles
{
get { return directoriesAndFiles; }
set { directoriesAndFiles = value; OnPropertyChanged(); }
}
private FileEntityViewModel? selectedFileEntity;
public FileEntityViewModel? SelectedFileEntity
{
get { return selectedFileEntity; }
set { selectedFileEntity = value; OnPropertyChanged(); }
}
#endregion
#region Commands
public ICommand OpenCommand { get; }
#endregion
#region Constructor
public MainWindowViewModel()
{
DirectoriesAndFiles = new ObservableCollection<FileEntityViewModel>();
OpenCommand = new DelegateCommand(Open);
foreach (var logicalDrive in Directory.GetLogicalDrives())
{
DirectoriesAndFiles.Add(new DirectoryViewModel(logicalDrive));
}
}
#endregion
#region CommandMethods
private void Open(object parameter)
{
if (parameter is DirectoryViewModel directoryViewModel)
{
CurrentFilePath = directoryViewModel.FullName;
DirectoriesAndFiles?.Clear();
var directoryInfo = new DirectoryInfo(CurrentFilePath ?? "");
foreach (var directory in directoryInfo.GetDirectories())
{
DirectoriesAndFiles?.Add(new DirectoryViewModel(directory));
}
foreach (var file in directoryInfo.GetFiles())
{
DirectoriesAndFiles?.Add(new FileViewModel(file));
}
}
}
#endregion
}
}
Every Binding works fine except this one <TextBox Grid.Row="0" Text="{Binding CurrentFilePath}"/>. The binding is to show the user current path, but the Text of TextBox doesn't change even if CurrentFilePath is changed.
What could be the reason of this? What am I doing wrong? Thanks!
Full project: https://github.com/CrackAndDie/Avalonia-MVVM-Explorer
The problem lies with your PropertyChanged notification.
internal class ViewModelBase : ReactiveObject
{
#region PropetryChangedHandler
public event PropertyChangedEventHandler? BasePropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
BasePropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
Nothing happens because ReactiveObject already implements INotifyPropertyChanged which WPF/Avalonia looks for which using Bind.
You must either change your view model to simply Implement INotifyPropertyChanged or change directly in your ViewModel to use ReactiveObject methods set => this.RaiseAndSetIfChanged(ref _memberField, value);
Since your ViewModel implements ReactiveObject it must use the operations provided by it to trigger change notifications. ReactiveObject is a class provided by ReactiveUI. ReactiveUI can be quite valuable to learn as it does provide many useful mechanisms to keep your code concise while implementing MVVM. However ReactiveUI is built on rx.net, understanding Rx.net is advisiable to use ReactiveUI, but can be a steep learning curve to start with.
I suggest Implementing INotifyPropertyChanged, which would result in the following in your ViewModelBase:
internal class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
If you stick to the ReactiveUI route then you would change:
public string? CurrentFilePath
{
get { return currentFilePath; }
set { currentFilePath = value; OnPropertyChanged(); }
}
To:
public string? CurrentFilePath
{
get => currentFilePath;
set => this.RaiseAndSetIfChanged(ref currentFilePath, value);
}
So what I want is when SelectedModel.TechName is updated that it physically saves to the model so that as long as the application is running it will remain whatever the user enters.
I have 2 views SelectedModel.TechName is called in both views. It pulls the data from the model however when I change views the data resets.
Any Suggestion?
edit: I am trying to make the data entered persistent, I thought setting the value would do this however every time i change between views it resets the data. In fact it blinks the data then resets it.
Field from DefaultView.Xaml
<StackPanel Grid.Row="0" Grid.Column="6" Grid.ColumnSpan="1" Margin="5 5 5 0">
<TextBox Name="techName" Text="{Binding SelectedModel.TechName,Mode=TwoWay}" BorderBrush="#FF4A5780" Grid.RowSpan="2"/>
</StackPanel>
<TextBlock x:Name="TextUpdate" Grid.Column="5" HorizontalAlignment="Left" Margin="41,0,0,0"
Grid.Row="1" Text="{Binding SelectedModel.TechName}" TextWrapping="Wrap" VerticalAlignment="Center"/>
DataModel.cs Model File
namespace callFlow.Models
{
public class DataModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string techName;
public DataModel()
{
}
public string TechName
{
get { return techName; }
set { techName = value;
OnPropertyChanged();
}
}
private void OnPropertyChanged([CallerMemberName] string techName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(techName));
}
}
}
DefaultViewModel.cs
namespace callFlow.ViewModels
{
public class DefaultViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public DefaultViewModel() { }
private ObservableCollection<DataModel> model = new ObservableCollection<DataModel>();
private DataModel selectedModel;
private DataModel _SelectedModel;
public DataModel SelectedModel
{
get { return _SelectedModel ?? (_SelectedModel = new SelectedModel()); }
set { _SelectedModel = value;
OnPropertyChanged(); }
}
public void changeSelectedModel(DataModel newSelectedModel)
{
SelectedModel.TechName = newSelectedModel.TechName;
}
private void OnPropertyChanged([CallerMemberName] string techNameVM = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(techNameVM));
}
}
}
On your binding you have
UpdateSourceTrigger=Explicit
in
Text="{Binding SelectedModel.TechName,Mode=TwoWay,
UpdateSourceTrigger=Explicit}"
When you do that, you have to write code to update the source property. Which is the viewmodel property.
Since you don't do that, the viewmodel will not get updated when you type text in there.
You should either remove that off the binding or write some more code.
There are multiple potential issues in your code. First, you use Explicit as UpdateSourceTrigger, but you never call UpdateSource, at least you do not show that in your code. Consequently, the property will never be updated. Use PropertyChanged or LostFocus instead.
If you set the UpdateSourceTrigger value to Explicit, you must call the UpdateSource method or the changes will not propagate back to the source.
Furthermore, you implement INotifyPropertyChanged in your view models, but you never call OnPropertyChanged. Hence, bindings will never be updated when a property changes its value. Your properties should look like below. This applies to all properties that you expose.
public string TechName
{
get { return techName; }
set
{
if (techName != value)
{
techName = value;
OnPropertyChanged();
}
}
}
It is not clear how you create your views and set their DataContext. If you create the data context view model in the XAML of your view, it will be created each time you instantiate a new view.
Simple solution
Remove the UpdateSourceTriger=Explicit from your DefaultView.xaml
<TextBox Name="techName" Text="{Binding SelectedModel.TechName,Mode=TwoWay}" BorderBrush="#FF4A5780" Grid.RowSpan="2"/>
Call the OnPropertyChanged method in the DataModel.TechName's setter. Like this:
public string TechName
{
get {
return techName;
}
set {
techName = value;
OnPropertyChanged();
}
}
Better solution
There are a few problems with your code. Here's how to fix them:
DefaultView.xaml
Remove the UpdateSourceTrigger=Explicit. It requires you to update the binding manually (from code) and you're not doing that.
<StackPanel Grid.Row="0" Grid.Column="6" Grid.ColumnSpan="1" Margin="5 5 5 0">
<TextBox Name="techName" Text="{Binding SelectedModel.TechName,Mode=TwoWay}" BorderBrush="#FF4A5780" Grid.RowSpan="2"/>
<TextBlock x:Name="TextUpdate" Grid.Column="5" HorizontalAlignment="Left" Margin="41,0,0,0"
Grid.Row="1" Text="{Binding SelectedModel.TechName}" TextWrapping="Wrap" VerticalAlignment="Center"/>
DataModel.cs
You were not calling the OnPropertyChanged method in TechName's setter, that's why it wasn't updating. I've done that and refactored the code a bit
public class DataModel : ObservableObject
{
private string _techName;
public string TechName
{
get => _techName;
set {
_techName = value;
OnPropertyChanged();
}
}
}
DefaultViewModel.cs
Here I've just removed the empty default constructor, the extra private DataModel field and refactored the code.
public class DefaultViewModel : ObservableObject
{
private ObservableCollection<DataModel> Models = new ObservableCollection<DataModel>();
private DataModel _selectedModel;
public DataModel SelectedModel
{
get => _selectedModel ?? (_selectedModel = new SelectedModel());
set {
_selectedModel = value;
OnPropertyChanged();
}
}
}
INotifyPropertyChanged implementation - ObservableObject.cs
I've added this class to simplify the rest of the code, since you were using the same code in both DataModel.cs and DefaultViewModel.cs
public class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
I'm facing a strange problem when using C# WPF and MVVM Pattern while restoring a ViewModel (serialized using Json.Net).
The idea of the software is - when closing the window - to persist the current Viewmodel state in a json file.
At the next startup the app just serarches for the json.
If there a file, then deserialize it and restore the ViewModel (set public properties).
If there is no file, then the viewmodel is created and default values are set.
Now my problem is, that when restoring it with the json file, a combobox containing a list of a custom type, the combobox has values but no SelectedItem. When creating the viewmodel instance and initiailizing the public properties with default values (doing this via the code behind) then everything is fine.
Here is some code that represents the "error":
View
<Window x:Class="CrazyWpf.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:CrazyWpf"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525"
Closing="Window_Closing"
Loaded="Window_Loaded">
<StackPanel
x:Name="rootElement"
Orientation="Vertical"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Margin="10">
<StackPanel.DataContext>
<local:DemoViewModel />
</StackPanel.DataContext>
<StackPanel
Orientation="Horizontal">
<Label
x:Name="lblID"
Width="30"
Content="ID:"/>
<TextBox
x:Name="tbID"
Width="50"
Margin="30,0,0,0"
Text="{Binding ID, UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
<StackPanel
Orientation="Horizontal">
<Label
x:Name="lblName"
Width="45"
Content="Name:"/>
<TextBox
x:Name="tbName"
Width="200"
Margin="15,0,0,0"
Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
<StackPanel
Orientation="Horizontal">
<Label
x:Name="lblStai"
Width="60"
Content="Status:"/>
<ComboBox
x:Name="cbStati"
Width="200"
ItemsSource="{Binding StatusTypeList}"
SelectedItem="{Binding StatusType, UpdateSourceTrigger=PropertyChanged}"
DisplayMemberPath="Name"/>
</StackPanel>
</StackPanel>
</Window>
Code Behind
using System;
using System.Windows;
using System.IO;
using Newtonsoft.Json;
namespace CrazyWpf
{
public partial class MainWindow : Window
{
private DemoViewModel dvm;
public MainWindow()
{
InitializeComponent();
this.dvm = (DemoViewModel)this.rootElement.DataContext;
}
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
string filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "settings.json");
if (File.Exists(filePath))
File.Delete(filePath);
File.WriteAllText(filePath, JsonConvert.SerializeObject(this.dvm, Formatting.Indented));
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
string filePath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "settings.json");
if (!File.Exists(filePath))
{ this.SetDefaultSettings(); return; }
DemoViewModel d = JsonConvert.DeserializeObject<DemoViewModel>(File.ReadAllText(filePath));
this.dvm.ID = d.ID;
this.dvm.Name = d.Name;
this.dvm.StatusType = d.StatusType;
}
}
}
BaseViewModel:
using System.ComponentModel;
namespace CrazyWpf
{
public abstract class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
ViewModel
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
namespace CrazyWpf
{
class DemoViewModel : BaseViewModel
{
[JsonIgnore]
private int id;
[JsonProperty(Order = 1)]
public int ID
{
get { return this.id; }
set
{
if (this.id != value)
{
this.id = value;
this.OnPropertyChanged("ID");
}
}
}
[JsonIgnore]
private string name;
[JsonProperty(Order = 2)]
public string Name
{
get { return this.name; }
set
{
if (this.name != value && value != null)
{
this.name = value;
this.OnPropertyChanged("Name");
}
}
}
[JsonIgnore]
private StatusTyp statusType;
[JsonProperty(Order = 3)]
public StatusTyp StatusType
{
get { return this.statusType; }
set
{
if (this.statusType != value && value != null)
{
this.statusType = value;
this.OnPropertyChanged("StatusType");
}
}
}
[JsonIgnore]
private List<StatusTyp> statusTypeList;
[JsonProperty(Order = 4)]
public List<StatusTyp> StatusTypeList
{
get { return this.statusTypeList; }
set
{
if (this.statusTypeList != value && value != null)
{
this.statusTypeList = value;
this.OnPropertyChanged("StatusTypeList");
}
}
}
public DemoViewModel()
{
this.StatusTypeList = new Func<List<StatusTyp>>(() =>
{
var list = Enum.GetValues(typeof(Status))
.Cast<Status>()
.ToDictionary(k => (int)k, v => v.ToString())
.Select(e => new StatusTyp()
{
Value = e.Key,
Name = e.Value,
Status =
Enum.GetValues(typeof(Status))
.Cast<Status>().
Where(x =>
{
return (int)x == e.Key;
}).FirstOrDefault()
})
.ToList();
return list;
})();
}
}
public class StatusTyp
{
public int Value { get; set; }
public string Name { get; set; }
public Status Status { get; set; }
}
public enum Status
{
NotDetermined = 0,
Determined = 1,
Undeterminded = 2,
Unknown = 3
}
}
If you have an ItemsSource and a SelectedItem, the instance in SelectedItem MUST BE in the collection bound to ItemsSource. If it is not, then your bindings will not work as expected.
The control uses reference equality to determine which item in ItemsSource is the one in SelectedItem and update the UI. This normally isn't a problem as the control populates SelectedItem for you, but if you are updating from the ViewModel side, you have to make sure your references are managed correctly.
This can be an issue when serializing/deserializing your view model. Most common serializers don't track references, and so cannot restore these on deserialization. The same object may be referenced multiple places in the original object graph, but after deserialization you now have multiple instances of the original spread throughout the rehydrated graph. This won't work with your requirements.
What you have to do is, after deserializing, find the matching instance in your collection and substitute it for the instance in SelectedItem. Or, use a serializer that tracks instances.. The XAML serializer already does this, and is a surprisingly good xml serializer for .net object graphs.
Question
How can I make it so that changes to a note are only propagated back to the list, when the Save button is clicked instead on "lost focus"?
And the Save button should only be enabled when the note has been changed.
UI
The example application looks like this:
The current behaviour is:
Clicking on a note puts its text into the TextBox; that's fine.
The changed text from the TextBox gets written back to the list when the TextBox loses the focus (default binding behaviour); but I only want that to happend when the Save button is clicked.
The Save button is always activated because the CanExecute(object parameter) isn't correctly implemented yet; it should only get activated when the TextBox text is different from the selected note's text.
My research so far
Option 1: Some Internet sources say to bind a different property to the TextBox and to programmatically check whether it is different from the SelectedItem of the ListView. I would have hoped that there was a way without introducing a third property in addition to the already existing ListOfNotes and SelectedNote.
Option 2: Some Internet sources recommend to configure Mode=OneWay so that clicking an item in the ListView updates the TextBox, but not the other way around. This sounds like the solution I would prefer, but I wasn't able to figure out from the code examples how to raise an event programmatically so that the change in the TextBox gets written back to the ListView when the Save button is clicked.
I've found other Stackoverflow questions that seem to be similar to mine, but the answers to those haven't helped me fix the problem:
WPF databinding after Save button click
Code
This example currently does two-way binding on focus lost. How do I need to change it to get the above described behaviour?
https://github.com/lernkurve/WpfBindingOneWayWithSaveButton
MainWindow.xaml
<Window x:Class="WpfBindingOneWayWithSaveButton.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:wpfBindingOneWayWithSaveButton="clr-namespace:WpfBindingOneWayWithSaveButton"
mc:Ignorable="d"
Title="MainWindow" Height="188.636" Width="299.242">
<Window.DataContext>
<wpfBindingOneWayWithSaveButton:MainWindowsViewModel />
</Window.DataContext>
<Grid>
<GroupBox Header="List of notes" HorizontalAlignment="Left" VerticalAlignment="Top" Height="112" Width="129" Margin="0,24,0,0">
<ListView ItemsSource="{Binding ListOfNotes}" SelectedItem="{Binding SelectedNote}" DisplayMemberPath="Text" HorizontalAlignment="Left" Height="79" VerticalAlignment="Top" Width="119" Margin="0,10,-2,0"/>
</GroupBox>
<GroupBox Header="Change selected note" HorizontalAlignment="Left" Margin="134,24,0,0" VerticalAlignment="Top" Height="112" Width="151">
<Grid HorizontalAlignment="Left" Height="89" Margin="0,0,-2,0" VerticalAlignment="Top" Width="141">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40*"/>
<ColumnDefinition Width="101*"/>
</Grid.ColumnDefinitions>
<TextBox Text="{Binding SelectedNote.Text}" HorizontalAlignment="Left" Height="23" TextWrapping="Wrap" VerticalAlignment="Top" Width="121" Margin="10,7,0,0" Grid.ColumnSpan="2"/>
<Button Command="{Binding SaveCommand}" Content="Save" HorizontalAlignment="Left" VerticalAlignment="Top" Width="121" Margin="10,35,0,0" Grid.ColumnSpan="2"/>
</Grid>
</GroupBox>
</Grid>
</Window>
MainWindowsViewModel.cs
using System.Collections.ObjectModel;
using System.Windows.Input;
namespace WpfBindingOneWayWithSaveButton
{
public class MainWindowsViewModel
{
public ObservableCollection<Note> ListOfNotes { get; set; }
public Note SelectedNote { get; set; }
public ICommand SaveCommand { get; set; }
public MainWindowsViewModel()
{
ListOfNotes = new ObservableCollection<Note>
{
new Note { Text = "Note 1" },
new Note { Text = "Note 2" }
};
SaveCommand = new SaveCommand(this);
}
}
}
SaveCommand.cs
using System;
using System.Windows.Input;
namespace WpfBindingOneWayWithSaveButton
{
public class SaveCommand : ICommand
{
private MainWindowsViewModel vm;
public SaveCommand(MainWindowsViewModel vm)
{
this.vm = vm;
}
public bool CanExecute(object parameter)
{
// What should go here?
return true;
// Pseudo code
// return (is the TextBox text different from the original note text)
}
public void Execute(object parameter)
{
// What should go here?
// Pseudo code
// Let WPF know that the TextBox text has changed
// Invoke the binding so it propagates the TextBox text back to the list
}
public event EventHandler CanExecuteChanged;
}
}
Note.cs
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace WpfBindingOneWayWithSaveButton
{
public class Note : INotifyPropertyChanged
{
private string text;
public string Text
{
get { return text; }
set
{
text = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Bind the text to the CommandParameter of the SaveButton so it gets passed to the Save method for updating.
<TextBox x:Name="NoteTextBox" Text="{Binding SelectedNote.Text, Mode=OneTime}" ../>
<Button Command="{Binding SaveCommand}"
CommandParameter="{Binding ElementName=NoteTextBox, Path=Text}",
Content="Save" />
and
public bool CanExecute(object parameter)
{
return vm.SelectedNote.Text != parameter as string;
}
public void Execute(object parameter)
{
vm.SelectedNote.Text = parameter as string;
}
Option one is the easiest to implement, you will need to clone the Note object and set it to a separate property.
in your xaml, change your list view to the following so it now binds the SelectedIndex instead of the SelectedItem.
<ListView ItemsSource="{Binding ListOfNotes}" SelectedIndex="{Binding SelectedIndex}" DisplayMemberPath="Text" ...
And change TextBox to the following so it updates the binding as you type
<TextBox Text="{Binding Path=SelectedNote.Text, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" ...
In Note.cs we add the Clone() method.
public class Note : INotifyPropertyChanged
{
public Note Clone()
{
return new Note()
{
Text = this.Text
};
}
//... The rest stays the same
}
In MainWindowsViewModel.cs we add new properties for the SelectedIndex and clone the object when we detect a index has changed. We also need to add INotifyPropertyChanged so we can update the SelectedNote from the codebehind when we do the Clone()
public class MainWindowsViewModel : INotifyPropertyChanged
{
private int _selectedIndex = -1;
private Note _selectedNote;
public int SelectedIndex
{
get { return _selectedIndex; }
set
{
if (_selectedIndex.Equals(value))
return;
_selectedIndex = value;
CloneSelectedNote();
}
}
private void CloneSelectedNote()
{
if (SelectedIndex >= 0)
{
SelectedNote = ListOfNotes[SelectedIndex].Clone();
}
else
{
SelectedNote = null;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public Note SelectedNote
{
get { return _selectedNote; }
set
{
if(Equals(_selectedNote, value))
return;
_selectedNote = value;
OnPropertyChanged();
}
}
//... The rest stays the same
}
In SaveCommand.cs we add the logic for CanExecute and add the subscriptions to CommandManager.RequerySuggested, this automatically makes it requery the CanExecute any time any binding changes. This can be a little ineffecent, if you wanted to you could expose a RaiseCanExecuteChanged() publicly but it would be MainWindowsViewModel responsibility to call it any time vm.SelectedIndex or vm.SelectedNote.Text changed.
public class SaveCommand : ICommand
{
private MainWindowsViewModel vm;
public SaveCommand(MainWindowsViewModel vm)
{
this.vm = vm;
}
public bool CanExecute(object parameter)
{
if (vm.SelectedIndex < 0 || vm.SelectedNote == null)
return false;
return vm.ListOfNotes[vm.SelectedIndex].Text != vm.SelectedNote.Text;
}
public void Execute(object parameter)
{
vm.ListOfNotes[vm.SelectedIndex] = vm.SelectedNote;
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
}
UPDATE: Here is a updated version that does not use CommandManager
MainWindowsViewModel.cs
public class MainWindowsViewModel : INotifyPropertyChanged
{
private int _selectedIndex = -1;
private Note _selectedNote;
public int SelectedIndex
{
get { return _selectedIndex; }
set
{
if (_selectedIndex.Equals(value))
return;
_selectedIndex = value;
CloneSelectedNote();
RecheckSaveCommand();
}
}
private void CloneSelectedNote()
{
if (SelectedIndex >= 0)
{
SelectedNote = ListOfNotes[SelectedIndex].Clone();
}
else
{
SelectedNote = null;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public Note SelectedNote
{
get { return _selectedNote; }
set
{
if(Equals(_selectedNote, value))
return;
if (_selectedNote != null)
{
PropertyChangedEventManager.RemoveHandler(_selectedNote, SelectedNoteTextChanged, nameof(Note.Text));
}
_selectedNote = value;
if (_selectedNote != null)
{
PropertyChangedEventManager.AddHandler(_selectedNote, SelectedNoteTextChanged, nameof(Note.Text));
}
OnPropertyChanged();
}
}
private void SelectedNoteTextChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs)
{
RecheckSaveCommand();
}
private void RecheckSaveCommand()
{
var command = this.SaveCommand as WpfBindingOneWayWithSaveButton.SaveCommand; //"this." and "WpfBindingOneWayWithSaveButton." are not necessary but I wanted to be explicit.
if (command != null)
{
command.RaiseCanExecuteChanged();
}
}
//...
}
SaveCommand.cs
public class SaveCommand : ICommand
{
private MainWindowsViewModel vm;
public SaveCommand(MainWindowsViewModel vm)
{
this.vm = vm;
}
public bool CanExecute(object parameter)
{
if (vm.SelectedIndex < 0 || vm.SelectedNote == null)
return false;
return vm.ListOfNotes[vm.SelectedIndex].Text != vm.SelectedNote.Text;
}
public void Execute(object parameter)
{
vm.ListOfNotes[vm.SelectedIndex] = vm.SelectedNote;
}
public event EventHandler CanExecuteChanged;
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
You should not use OneWay but rather an UpdateSourceTrigger of value Explicit. BindingGroups can do this for you though, here's a simple example:
<!-- For change observation -->
<TextBlock Text="{Binding Text}"></TextBlock>
<StackPanel>
<StackPanel.BindingGroup>
<BindingGroup x:Name="EditGroup"></BindingGroup>
</StackPanel.BindingGroup>
<TextBox Text="{Binding Text}"></TextBox>
<Button>
<Button.Command>
<local:CommitGroupCommand BindingGroup="{x:Reference EditGroup}"/>
</Button.Command>
Save
</Button>
</StackPanel>
public class CommitGroupCommand : ICommand
{
public BindingGroup BindingGroup { get; set; }
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
BindingGroup.UpdateSources();
}
}
(You could add a validation rule to your binding that requires the value to be different and use that for the CanExecute implementation.)
Using this method allows you to bind directly to the object you intend to edit, so you don't need to copy around values first.
I'm trying to make Avalon MVVM compatible in my WPF application. From googling, I found out that AvalonEdit is not MVVM friendly and I need to export the state of AvalonEdit by making a class derived from TextEditor then adding the necessary dependency properties. I'm afraid that I'm quite lost in Herr Grunwald's answer here:
If you really need to export the state of the editor using MVVM, then I suggest you create a class deriving from TextEditor which adds the necessary dependency properties and synchronizes them with the actual properties in AvalonEdit.
Does anyone have an example or have good suggestions on how to achieve this?
Herr Grunwald is talking about wrapping the TextEditor properties with dependency properties, so that you can bind to them. The basic idea is like this (using the CaretOffset property for example):
Modified TextEditor class
public class MvvmTextEditor : TextEditor, INotifyPropertyChanged
{
public static DependencyProperty CaretOffsetProperty =
DependencyProperty.Register("CaretOffset", typeof(int), typeof(MvvmTextEditor),
// binding changed callback: set value of underlying property
new PropertyMetadata((obj, args) =>
{
MvvmTextEditor target = (MvvmTextEditor)obj;
target.CaretOffset = (int)args.NewValue;
})
);
public new string Text
{
get { return base.Text; }
set { base.Text = value; }
}
public new int CaretOffset
{
get { return base.CaretOffset; }
set { base.CaretOffset = value; }
}
public int Length { get { return base.Text.Length; } }
protected override void OnTextChanged(EventArgs e)
{
RaisePropertyChanged("Length");
base.OnTextChanged(e);
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
Now that the CaretOffset has been wrapped in a DependencyProperty, you can bind it to a property, say Offset in your View Model. For illustration, bind a Slider control's value to the same View Model property Offset, and see that when you move the Slider, the Avalon editor's cursor position gets updated:
Test XAML
<Window x:Class="AvalonDemo.TestWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avalonEdit="http://icsharpcode.net/sharpdevelop/avalonedit"
xmlns:avalonExt="clr-namespace:WpfTest.AvalonExt"
DataContext="{Binding RelativeSource={RelativeSource Self},Path=ViewModel}">
<StackPanel>
<avalonExt:MvvmTextEditor Text="Hello World" CaretOffset="{Binding Offset}" x:Name="editor" />
<Slider Minimum="0" Maximum="{Binding ElementName=editor,Path=Length,Mode=OneWay}"
Value="{Binding Offset}" />
<TextBlock Text="{Binding Path=Offset,StringFormat='Caret Position is {0}'}" />
<TextBlock Text="{Binding Path=Length,ElementName=editor,StringFormat='Length is {0}'}" />
</StackPanel>
</Window>
Test Code-behind
namespace AvalonDemo
{
public partial class TestWindow : Window
{
public AvalonTestModel ViewModel { get; set; }
public TestWindow()
{
ViewModel = new AvalonTestModel();
InitializeComponent();
}
}
}
Test View Model
public class AvalonTestModel : INotifyPropertyChanged
{
private int _offset;
public int Offset
{
get { return _offset; }
set
{
_offset = value;
RaisePropertyChanged("Offset");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
You can use the Document property from the editor and bind it to a property of your ViewModel.
Here is the code for the view :
<Window x:Class="AvalonEditIntegration.UI.View"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:AvalonEdit="clr-namespace:ICSharpCode.AvalonEdit;assembly=ICSharpCode.AvalonEdit"
Title="Window1"
WindowStartupLocation="CenterScreen"
Width="500"
Height="500">
<DockPanel>
<Button Content="Show code"
Command="{Binding ShowCode}"
Height="50"
DockPanel.Dock="Bottom" />
<AvalonEdit:TextEditor ShowLineNumbers="True"
Document="{Binding Path=Document}"
FontFamily="Consolas"
FontSize="10pt" />
</DockPanel>
</Window>
And the code for the ViewModel :
namespace AvalonEditIntegration.UI
{
using System.Windows;
using System.Windows.Input;
using ICSharpCode.AvalonEdit.Document;
public class ViewModel
{
public ViewModel()
{
ShowCode = new DelegatingCommand(Show);
Document = new TextDocument();
}
public ICommand ShowCode { get; private set; }
public TextDocument Document { get; set; }
private void Show()
{
MessageBox.Show(Document.Text);
}
}
}
source : blog nawrem.reverse
Not sure if this fits your needs, but I found a way to access all the "important" components of the TextEditor on a ViewModel while having it displayed on a View, still exploring the possibilities though.
What I did was instead of instantiating the TextEditor on the View and then binding the many properties that I will need, I created a Content Control and bound its content to a TextEditor instance that I create in the ViewModel.
View:
<ContentControl Content="{Binding AvalonEditor}" />
ViewModel:
using ICSharpCode.AvalonEdit;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Highlighting;
// ...
private TextEditor m_AvalonEditor = new TextEditor();
public TextEditor AvalonEditor => m_AvalonEditor;
Test code in the ViewModel (works!)
// tests with the main component
m_AvalonEditor.SyntaxHighlighting = HighlightingManager.Instance.GetDefinition("XML");
m_AvalonEditor.ShowLineNumbers = true;
m_AvalonEditor.Load(#"C:\testfile.xml");
// test with Options
m_AvalonEditor.Options.HighlightCurrentLine = true;
// test with Text Area
m_AvalonEditor.TextArea.Opacity = 0.5;
// test with Document
m_AvalonEditor.Document.Text += "bla";
At the moment I am still deciding exactly what I need my application to configure/do with the textEditor but from these tests it seems I can change any property from it while keeping a MVVM approach.