I've done some research on the topic, and while I've come across some possibilities, nothing has worked for me.
Details:
I'm working on a WPF app using an MVVM design pattern. In the ViewModel, I have a List of Notes, a class with a few properties (among them, Note). I've created a property, SelectedNote on the VM to hold the currently selected note.
In my View, I've bound a ListView control to the list QcNotes. I've bound a TextBox to the SelectedNote property. When I make changes to the TextBox, they are correctly reflected in the appropriate row of the ListView.
Problem:
I've include a RevertChanges command. This is a relatively simple command that undoes changes I've made to the note. It correctly updates the TextBox, and it actually updates the underlying list correctly, but the changes do not update the ListView itself. (Is it necessary to use an ObservableCollection in this circumstance? I've been asked to try and resolve the problem without doing so).
Attempted Fixes
I tried to call NotifyPropertyChanged("SelectedNote") and NotifyPropertyChanged("QcNotes") directly from within the call to RevertChanges, but that hasn't fixed the problem.
Any ideas?
XAML
<Window.DataContext>
<VM:MainProjectViewModel />
</Window.DataContext>
<Grid>
<StackPanel>
<ListView ItemsSource="{Binding QcNotes, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" x:Name="list" SelectedItem="{Binding SelectedNote}">
<ListView.View>
<GridView>
<GridViewColumn Header="Note" DisplayMemberBinding="{Binding Note}" />
</GridView>
</ListView.View>
</ListView>
<TextBox
Height="30"
HorizontalAlignment="Stretch"
Text="{Binding SelectedNote.Note, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Content="Allow Edits" Command="{Binding ChangeStateToAllowEditsCommand}" />
<Button Content="Save Changes" Command="{Binding EditNoteCommand}" />
<Button Content="Revert Changes" Command="{Binding RevertChangesToNoteCommand}" />
</StackPanel>
</StackPanel>
</Grid>
ViewModel Code
public class MainViewModel : BaseViewModel
{
private QcNote selectedNote;
private string oldNoteForUpdating;
private VMState currentState;
private string noteInput;
private IList<QcNote> qcNotes;
public IList<QcNote> QcNotes
{
get
{
return qcNotes;
}
set
{
qcNotes = value;
NotifyPropertChanged();
}
}
public QcNote SelectedNote
{
get
{
return selectedNote;
}
set
{
selectedNote = value;
oldNoteForUpdating = SelectedNote.Note;
NotifyPropertChanged();
}
}
public VMState CurrentState
{
get
{
return currentState;
}
set
{
currentState = value;
NotifyPropertChanged();
}
}
public ICommand RevertChangesToNoteCommand
{
get
{
return new ActionCommand(o => RevertChangestoNote());
}
}
private void RevertChangestoNote()
{
QcNotes.First(q => q.Id == SelectedNote.Id).Note = oldNoteForUpdating;
SelectedNote.Note = oldNoteForUpdating;
NotifyPropertChanged("SelectedNote");
NotifyPropertChanged("QcNotes");
CurrentState = VMState.View;
}
I'll post an answer to my own question, but don't want to deter other from offering suggestions.
I implemented the INotifyPropertyChanged interface on my Models.QcNote class, and that resolved the issue. Initially, the interface was implemented exclusively on the ViewModel. In that case, NotifyPropertyChanged was only called when the QcNote object itself was changed, not when the properties of the object were changed.
Related
I am learning MVVM pattern while refactoring an app to MVVM.
I have a model class Machine that provides a list of installations in a form of ObservableCollection<Installation> Installations.
In one of the windows (views) I need to display only those installations that have updates (thus meet the following criteria):
private void InstallationsToUpdateFilter(object sender, FilterEventArgs e)
{
var x = (Installation)e.Item;
bool hasNewVersion = ShowAllEnabledInstallations ? true : x.NewVersion != null;
bool isSetAndOn = !String.IsNullOrEmpty(x.Path) && x.CheckForUpdatesFlag;
e.Accepted = isSetAndOn && hasNewVersion;
}
private void OnFilterChanged()
{
installationsToUpdateSource?.View?.Refresh();
}
I am doing this by filtering in my ViewModel:
class NewVersionViewModel : ViewModelBase
{
private Machine machine = App.Machine;
...
public NewVersionViewModel(...)
{
...
InstallationsToUpdate.CollectionChanged += (s, e) =>
{
OnPropertyChanged("NewVersionsAvailableMessage");
OnFilterChanged();
};
installationsToUpdateSource = new CollectionViewSource();
installationsToUpdateSource.Source = InstallationsToUpdate;
installationsToUpdateSource.Filter += InstallationsToUpdateFilter;
}
public ObservableCollection<Installation> InstallationsToUpdate
{
get { return machine.Installations; }
set { machine.Installations = value; }
}
internal CollectionViewSource installationsToUpdateSource { get; set; }
public ICollectionView InstallationsToUpdateSourceCollection
{
get { return installationsToUpdateSource.View; }
}
...
}
This is done by custom ListView:
<ListView ItemsSource="{Binding InstallationsToUpdateSourceCollection}" ... >
...
<ListView.ItemTemplate>
<DataTemplate>
<Grid ...>
<Grid ...>
<CheckBox Style="{StaticResource LargeCheckBox}"
IsChecked="{Binding Path=MarkedForUpdate, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
IsEnabled="{Binding Path=HasNewVersion}"
/>
</Grid>
<Label Content="{Binding Path=InstalledVersion.Major}" Grid.Column="1" Grid.Row="0" FontSize="50" FontFamily="Segoe UI Black" HorizontalAlignment="Center" VerticalAlignment="Top" Margin="0,-10,0,0"/>
...
<Grid.ContextMenu>
<ContextMenu>
...
</ContextMenu>
</Grid.ContextMenu>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
All of this works - until I try to "send" <CheckBox IsChecked="{Binding Path=MarkedForUpdate... back to my model - so it will be stored there.
How it can be done? (Can I have some kind of setter on ICollectionView?)
Current architecture can be changed. What I ultimately need:
Display items (installations) from model in ListView (currently: works)
Filter/Show only installations that meet some criteria (currentrly: works)
Reflect changes in MarkedForUpdate checkbox back to model (currently: not working)
I've googled a lot but was unable to find a relevant solution or suggestions.
Any help would be greatly appreciated. Thanks!
I figured the problem out. Although it was a silly mistake, I still want to share it to save someone's time.
The model itself updates in the configuration described above. The problem was that what model property (Machine.Installations in my case) did not implement INotifyPropertyChanged interface so other Views (through their corresponding ViewModels) were not aware of changes. Thus one should use OnPropertyChanged/RaisePropertyChanged not only in ViewModel, but in Model as well.
Hope this may help someone.
I am using Caliburn.Micro to try to bind items in a ListBox to one of two views but a single viewmodel. I am able to display the items in the ListBox, but when any item is selected I get 'Cannot find view for CustomerViewModel.'
Here are the relevant pieces of this application:
AppBootstrapper:
public class AppBootstrapper : BootstrapperBase
{
public AppBootstrapper()
: base()
{
Initialize();
}
protected override void OnStartup(object sender, StartupEventArgs e)
{
base.DisplayRootViewFor<CustomerWorkspaceViewModel>();
}
}
In my Views/Customers folder I have a CustomerViewModel:
public class CustomerViewModel : Screen
{
private string name;
public string Name
{
get { return name; }
set
{
name = value;
NotifyOfPropertyChange(() => Name);
}
}
}
and a CustomerWorkspaceViewModel:
public class CustomerWorkspaceViewModel : DocumentWorkspace<CustomerViewModel>
{
private CustomerViewModel selectedItem;
public CustomerViewModel SelectedItem
{
get { return selectedItem; }
set
{
selectedItem = value;
NotifyOfPropertyChange(() => SelectedItem);
}
}
public CustomerWorkspaceViewModel()
{
Items.AddRange(
new ObservableCollection<CustomerViewModel>
{
new CustomerViewModel {Name = "Customer 1" },
new CustomerViewModel {Name = "Customer 2" },
new CustomerViewModel {Name = "Customer 3" }
});
}
}
I have four views in my Views/Customers folder:
In a Views/Customers/CustomerWorkspace, I have and Edit View and an Read view:
Edit.xaml:
<Grid>
<StackPanel>
<Label Content="Edit View"/>
<TextBlock Foreground="White"
FontSize="20"
Text="{Binding Name}" />
</StackPanel>
</Grid>
and Read.xaml:
<Grid>
<TextBlock Foreground="White"
FontSize="20"
Text="{Binding Name}" />
<Label Content="Read view"/>
</Grid>
Finally I have an empty CustomerView user control in Views/Customers, and a CustomerWorkspaceView in Views/Customers:
<Grid>
<ListBox x:Name="Items"
Margin="5"
SelectedItem="{Binding SelectedItem}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" Margin="5" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<ContentControl cal:View.Context="{Binding State, Mode=TwoWay}" cal:View.Model="{Binding SelectedItem}"/>
</Grid>
Finally I have DocumentWorkspace, at the root folder, with AppBootstrapper:
public abstract class DocumentWorkspace<TDocument> : Conductor<TDocument>.Collection.OneActive
where TDocument : class, INotifyPropertyChanged, IDeactivate, IHaveDisplayName
{
public enum DocumentWorkspaceState
{
Read,
Edit
}
DocumentWorkspaceState state = DocumentWorkspaceState.Read;
public DocumentWorkspaceState State
{
get { return state; }
set
{
if (state == value)
return;
state = value;
NotifyOfPropertyChange(() => State);
}
}
}
What I am desiring (and expecting) is when selecting an item in the ListBox, which is composed of DocumentWorkspace objects, which are Conductors, to switch from one view (Edit) to another (Read). The select is working, the SelectedItem setter is getting fired, and the State in DocumentWorkspace is set correctly. But Caliburn.Micro cannot seem to find the view for the resulting CustomerViewModel that is SelectedItem. I've really tried to include in this post only what is needed to reproduce the problem here.
Note the documentation for what I am trying to do follows the discussion at
So mvermef pointed me in the right direction. I needed to fix my namespaces. I also needed to create a folder called "Customers" under "Views" and move the Edit and Read views to it. Then I needed to fix my namespaces again. Resharper is a great tool for that, by the way.
So what was wrong, besides the namespaces? In the CustomerWorkspaceView, I had
<ContentControl cal:View.Context="{Binding State, Mode=TwoWay}" cal:View.Model="{Binding SelectedItem}"/>
This caused Caliburn.Micro to look for a State property in the CustomerWorkspaceViewModel (actually in the base class DocumentWorkspace) and in this case the result was "Read". It just needs a string here. There was a "Read" View, but it was in a folder called "CustomerWorkspace". It needed to be in a folder called Customer, as the SelectedItem property is of type CustomerViewModel.
It turns out I didn't even need the empty CustomerView. As long as Caliburn.Micro can find a folder with the same name as your ViewModel type minus "ViewModel" and the namespaces match the folder structure, it should find your view.
Here's my complete folder structure:
I have strange problem, my Textbox in ListView's DataTemplate wont update its data. Data are setted in my property "LastValue" but it was never return.
Here is my ViewModel code (only important parts of this class):
public interface ISignal : IValue, IChartItem, INotifyPropertyChanged
{
string SignalName { get; set; }
}
[Serializable]
public class Signal : ObservableObject, ISignal
{
public Signal()
: this(new ModelsDialogService())
{
LastValue = 0.0;
}
public Signal(IDialogService dialog)
{
dialogService = dialog;
VisibleInGraph = true;
RefreshRate = 1000;
Include = true;
Color = ColorList.FirstOrDefault();
LastValue = 0.0;
}
private readonly List<SignalValue> values = new List<SignalValue>();
[XmlIgnore]
public IEnumerable<SignalValue> Values
{
get
{
return values;
}
}
private double lastValue;
[XmlIgnore]
public double LastValue
{
get
{
return lastValue;
}
set
{
Set(ref lastValue, value);
//RaisePropertyChanged(() => LastValue);
}
}
public void AddValue(SignalValue val)
{
values.Add(val);
ValueAdded(this, new ValueAddedEventArgs(val));
LastValue = Convert.ToDouble(((XYValue)val).Value);
}
}
And my XAML:
<ListView ItemsSource="{Binding SignalGroup.Signals}" SelectedValue="{Binding SelectedSignal}" FontWeight="Normal" BorderThickness="0" Foreground="white" HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch" DockPanel.Dock="Top" Background="#FF5B5A5A" Margin="10" >
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Style.Resources>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}"
Color="{StaticResource MetroBlueColor}"/>
</Style.Resources>
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu ItemsSource="{Binding CommandList}">
<ContextMenu.ItemTemplate >
<DataTemplate>
<MenuItem Header="{Binding DisplayName}" Command="{Binding ContextMenuCommand}" />
</DataTemplate>
</ContextMenu.ItemTemplate>
</ContextMenu>
</Setter.Value>
</Setter>
<Setter Property="HorizontalAlignment" Value="Stretch" />
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate>
<DockPanel HorizontalAlignment="Stretch">
<TextBlock Text="{Binding SignalName}" DockPanel.Dock="Left" />
<TextBlock Text="{Binding LastValue}" TextAlignment="Right" Margin="10,0,10,0" DockPanel.Dock="Right"/>
</DockPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Thanks for any idea.
Oh, i found interesting fact. Binding doesn't work only after deserialization. When I create new structure etc.. it works but when I serialize this structure to XML using XMLSerializer and then deserialize every binding in this class doesen't work, so I can change all values but its not updated in GUI... Weird
I have implemented a very small MVVM example.
Lets assume that the Signal class has only the two attributes you want to bind to. Then Signal looks very clean and easy:
public class Signal
{
public string SignalName { get; set; }
public double LastValue { get; set; }
}
Obviously Signal is your Model!
Now you need the ViewModel which has the name name in my small test application:
public class ViewModel
{
public ViewModel()
{
this.Signals = new ObservableCollection<Signal>();
this.Signals.Add(new Signal() { LastValue = 12432.33, SignalName = "First Signal"} );
this.Signals.Add(new Signal() { LastValue = 2.123, SignalName = "Second Signal"});
}
public ObservableCollection<Signal> Signals { get; set; }
}
An ObservableCollection is like a list with the difference that the View is notified when the Collection changes. The collection changes with operations like .Add(...) and .Remove(...).
The View looks similar to yours. i have choosen a GridView, because it is much more handy because it supports features like sorting and editing of the elements:
<DataGrid ItemsSource="{Binding Signals}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="LastName" Binding="{Binding SignalName}" />
<DataGridTextColumn Header="LastName" Binding="{Binding LastValue}" />
</DataGrid.Columns>
</DataGrid>
You may want to set the IsReadOnly to True when you are using the GridView
A solution with a ListView should look the same!
The Result:
Make sure, that you use the MVVM Pattern correctly. The Model only holds the data. The ViewModel is for all the business logic and the View only shows the data.
I would also recommend to create a folder structure so you have better overview over your solution. It also makes it easier to follow the MVVM pattern.
Hope it helps and clarified MVVM
Thank you for your request, it is really useful, but I´ve found where the problem is.
I have two collections in my application. I add Signals to first and when user wants to monitor some of these signals, selected signals are putted to the second collection too(but only its reference). Serialization creates the XML from this structure and deserialization overlooks the references and creates a new object of signal in first and also second collection. And here we go! :-D
I feel really stupid and dumb after dicovering this. I must refactor it till I forgot it. I spent a lot of time by searching the cause of this bug.
Thank for your request anyway!
I'm trying to get a WPF MVVM template to work with the basic functionality I am doing in a WPF but non MVVM application. In this case I am trying to capture the RowEditEnding event (which I am) to validate the data on the row that has changed (and this is the problem).
In the XAML I have used an event trigger:
<DataGrid AutoGenerateColumns="False" HorizontalAlignment="Stretch" Margin="0,0,0,0" VerticalAlignment="Stretch"
ItemsSource="{Binding oDoc.View}">
<DataGrid.Columns>
<DataGridTextColumn x:Name="docIDColumn" Binding="{Binding DocId}" Header="ID" Width="65"/>
<DataGridTextColumn x:Name="DocumentNumberColumn" Binding="{Binding Number}" Header="Document Number" Width="*"/>
<DataGridTextColumn x:Name="altIDColumn" Binding="{Binding AltID, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Header="Alt" Width="55"/>
</DataGrid.Columns>
<i:Interaction.Triggers>
<i:EventTrigger EventName="RowEditEnding">
<i:InvokeCommandAction Command="{Binding DocRowEdit}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</DataGrid>
With a delegate command to rout to the handler:
public ObservableCollection<Document> oDoc
{
get
{
return _oDoc;
}
}
public ICommand DocRowEdit
{
get { return new DelegateCommand(DocumentRowEditEvent); }
}
public void DocumentRowEditEvent()
{
//How do I find the changed item?
int i = 1;
}
I have not found a way to find the member of the ObservableCollection (oDoc) that has pending changes. I notice that the datagrid is doing some validation, the AltID field that I want to change will highlight red if I put in a non numeric value. But I want to handle the validation, and associated messaging myself. What am I missing? I was thinking to somehow raise a property changed event, but don't find how to wire something like this in:
protected void RaisePropertyChangedEvent(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
The last two code blocks are from my ViewModel class, and I'm trying to do this without any code behind, aside from instantiating the ViewModel in the MainWindow constructor.
You can add a property to your ViewModel:
public Document CurrentDocument
{
get
{
return _currentDocument;
}
set
{
if (value != _currentDocument)
{
_currentDocument = value;
OnPropertyChanged("CurrentDocument") // If you implement INotifyPropertyChanged
}
}
}
and then you can bind it to SelectedItem property of your DataGrid:
<DataGrid AutoGenerateColumns="False" HorizontalAlignment="Stretch" Margin="0,0,0,0" VerticalAlignment="Stretch"
ItemsSource="{Binding oDoc.View}" SelectedItem="{Binding CurrentDocument}">
Therefore your edit method will be:
public void DocumentRowEditEvent()
{
CurrentDocument.Number = DateTime.Now.Ticks;
/* And so on... */
}
I hope it helps.
I got used to implement event handlers to set a specific control's property, by creating a checking method and calling it in every handler, like that:
private void checkProperties()
{
myButton.IsEnabled = !String.IsNullOrWhiteSpace(myTextBox.Text) && myComboBox.SelectedIndex > -1;
}
private void myTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
checkProperties();
}
private void myComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
checkProperties();
}
and
<Window x:Class="MyProgram.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<Grid>
<TextBox Height="23" HorizontalAlignment="Left" Name="myTextBox" VerticalAlignment="Top" Width="120" TextChanged="myTextBox_TextChanged" />
<ComboBox Height="23" HorizontalAlignment="Left" Margin="0,38,0,0" Name="myComboBox" VerticalAlignment="Top" Width="120" SelectionChanged="myComboBox_SelectionChanged" />
<Button Content="Button" Height="23" HorizontalAlignment="Left" Margin="45,84,0,0" Name="myButton" VerticalAlignment="Top" Width="75" IsEnabled="False" />
</Grid>
</Window>
But that gets pretty heavy and redundant when you have a property that depends on 10 or more other controls' properties (just think about a Wizard window and its "Next" button, which should only be enabled if every controls are valid).
Is there a way to modify a property to automatically change depending on other controls' properties?
I've read about Dependency Properties a bit, but I'm not sure I can, let's say, modify the "IsEnabled" property of myButton to meet my expectations.
You don't have to handle events. You should be using element binding, which automatically bind to target properties values. However, both properties in question should be a Dependancy property. It should work in your case.
See this example:
http://msdn.microsoft.com/en-us/library/system.windows.data.binding.elementname(v=vs.110).aspx
Consider using MVVM instead of code behind. Enabling a button is command logic that is testable, and could be handled very simply in the view model via an ICommand.CanExecute delegate. No binding would be required since WPF automatically calls CanExecute() when the UI changes.
class MyViewModel : INotifyPropertyChanged
{
public ICommand SomeCommand { get; private set; }
public string Text { get; set; } //INPC omited for brevity
public int SelectedIndex { get; set; } //INPC omited for brevity
public MyViewModel()
{
SomeCommand = new RelayCommand(DoSomeCommand, CanDoSomeCommand);
}
private void DoSomeCommand()
{
//Blah
}
private bool CanDoSomeCommand()
{
return !String.IsNullOrWhiteSpace(this.Text) && this.SelectedIndex > -1;
}
}
WPF Multibinding is probably what you need in similar situations - Multibinding WPF.
<Grid>
...
<Grid.Resources>
<somenamespace:ValidatorConverter x:Key="ValidatorConverterResource">
</Grid.Resources>
<TextBox Name="myTextBox" ... />
<ComboBox Name="myComboBox" ... />
<Button Name="myButton" ...>
<Button.IsEnabled>
<MultiBinding Converter="{StaticResource ValidatorConverterResource}"
ConverterParameter="Left">
<Binding Path="Text"
ElementName="myTextBox" />
<Binding Path="SelectedIndex"
ElementName="{myComboBoxSelf}" />
</MultiBinding>
</Button.IsEnabled>
</Button>
</Grid>
But it won't actually ease the problem (it will just move it from code-behind to converters):
public class ValidatorConverter: IConverter
{
public object Converter( ...) //...
public object ConverterBack( ...) //...
}
About the validation goal
There is possibly some easy way to do it, but I don't know it, so the easiest way to at least consolidate the issue to where it belongs is to use bindings and MVVM design pattern.
In the model(viewmodel) make some IsValid property(and bind it to the Next IsEnabled directly or even better - through the command) , that will be updated according to the current values of bound properties.
public class WizardViewModel : INotifyPropertyChanged
{
// OnPropertyChanged(String) and PropertyChanged event
public String Text
{
get //..
set
{
this._Text = value;
Validate();
this.OnPropertyChanged("SelectedItem")
}
}
public Object SelectedItem
{
get //..
set
{
this._SelectedItem = value;
Validate();
this.OnPropertyChanged("SelectedItem")
}
}
public bool IsValid { // ...}
private void Validate()
{
if (String.IsNullOrEmpty(this._Text))
this.IsValid = false;
// ....
}
}
And in xaml:
<somenamespace:WizardViewModel x:Key="WizardViewModelInstance">
// ....
<Grid.DataContext="{StaticResource WizardViewModelInstance}">
// ....
<TextBox ... Text="{Binding Text}"/>
<ComboBox ... SelectedItem="{Binding SelectedItem}"/>
<Button ... IsEnabled="{Binding IsEnabled}"/>
Such solution will decouple your logic from view and put everything related to such validation into one place.
Also:
If you want to learn about WPF validation you may want to read http://msdn.microsoft.com/en-us/library/ms753962(v=vs.110).aspx
If you want to learn about WPF commands, read http://msdn.microsoft.com/en-us/magazine/dn237302.aspx