What's the right approach to open a child window (for example, to modify a selected item on the main window) keeping MVVM in mind?
Here's what I have: MainWindow.xaml (and in MainWindow.xaml.cs it assigns MainVM as its own DataContext)
I would also like to have: ChildWindow.xaml and barebones ChildWindow.xaml.cs with ChildVM behind controls.
So, now:
How can I popup ChildWindow and pass some object Data to its
ChildVM?
Get the result (true/false) and result data (some complex
object) back to MainVM?
As a bonus, can changes in Data be observed
by MainVM while they are being worked on by ChildVM?
Here's what I tried - it doesn't solve everything, but is this even the right direction?
For (2), I created a subclass of Window, called DialogWindow, which has 3 DependencyProperties: Data (for input data), ResultData (for output data) and ResultValue (for a bool result).
ResultData and ResultValue are both set by the ChildVM of DialogWindow using Binding, and when ResultValue is set, the DialogWindow closes.
At the moment, the ChildWindow is launched (for all intents and purposes) from MainWindow.xaml.cs - kinda bad. I can then pass some input data, like so:
ChildDialogWindow w = new ChildDialogWindow();
w.Data = myDataObj;
So, now I need to have a property Data on ChildVM, and set in ChildDialogWindow.xaml.cs. Again, making .xaml.cs thicker.
I thought that maybe a better approach that avoids MainWindow.xaml.cs would be some kind of DialogService which is passed to MainVM as a dependency. But then, how can I pass values to the ChildVM?
Try this.
Make a DialogService.cs
public class DialogService
{
public void Show(FrameworkElement view, ChildViewModel ChildVM)
{
Window window = new Window();
window.Content = view;
window.DataContext = ChildVM;
// For closing this dialog using MVVM
ChildVM.RequestClose += delegate
{
window.Close();
};
window.Show();
}
}
Now in ChildVm class, add this
public ICommand CloseCommand
{
get
{
if (_closeCommand == null)
_closeCommand = new RelayCommand(param => this.OnRequestClose());
return _closeCommand;
}
}
public event EventHandler RequestClose;
void OnRequestClose()
{
EventHandler handler = this.RequestClose;
if (handler != null)
handler(this, EventArgs.Empty);
}
Now, this the way to launch this
public void OpenChildDailog()
{
DialogService service = new DialogService();
ChildViewModel childVM = new ChildViewModel();
childVM.Data = ; // Assign whatever you want
childVM.ResultData = ;
service.Show(new ChildView(), childVM);
// Now get the values when the child dailog get closed
var retVal = childVM.ResultValue;
}
I'm using the ICommand helper "RelayCommand," and pushing an IntPtr datatype to the new ViewModel (or use any other object.) Lots of cookie cutter stuff.
Main View:
<Button Command="{Binding DataContext.ShowObjectInfoCommand}" CommandParameter="{Binding ObjectOffset}" Content="{Binding Name}"/>
MainViewModel:
private RelayCommand _showObjectInfoCommand;
public RelayCommand ShowObjectInfoCommand { get { return _showObjectInfoCommand ?? (_showObjectInfoCommand = new RelayCommand(ExeShowObjectInfoCommand)); } set { } } //Draw Specific Item Table
void ExeShowObjectInfoCommand(object parameter)
{
ViewObjectInfo objInfo = new ViewObjectInfo();
IObjectOffsetParameter viewModel = objInfo.DataContext as IObjectOffsetParameter;
viewModel.ObjectOffset = (IntPtr)parameter;
objInfo.Show();
}
New ViewModel + interface:
interface IObjectOffsetParameter
{
IntPtr ObjectOffset { get; set; }
}
class ViewModelObjectInfo : ViewModelBase, IObjectOffsetParameter
{
public ViewModelObjectInfo()
{
}
private IntPtr _objectOffset; //Entity Offset
public IntPtr ObjectOffset
{
get { return _objectOffset; }
set { if (_objectOffset != value) { _objectOffset = value; RaisePropertyChanged("Offset"); } }
}
}
New View code-behind:
InitializeComponent();
ViewModelObjectInfo viewModel = new ViewModelObjectInfo();
this.DataContext = viewModel;
New View xaml:
<TextBlock Text="{Binding ObjectOffset}"/>
Related
I have a WPF application and I want the Start button control only enabled if they have to have specified a value in the text box for 'Download Path'.
My ViewModel contains a property for my model "ConfigurationSettings" and an ICommand implementation (CommandImp) for the button:
public class MainWindowViewModel : BaseNotifyPropertyChanged
{
private ConfigurationSettings _configurationSettings { get; set; }
public ConfigurationSettings ConfigurationSettings
{
get
{
return _configurationSettings;
}
set
{
if (_configurationSettings != value)
{
_configurationSettings = value;
RaisePropertyChanged("ConfigurationSettings");
}
}
}
public CommandImp StartCommand { get; set; } // this is an implementation of ICommand
public MainWindowViewModel()
{
StartCommand = new CommandImp(OnStart, CanStart);
_configurationSettings = new ConfigurationSettings();
_configurationSettings.PropertyChanged += delegate (object o,
PropertyChangedEventArgs args)
{
StartCommand.RaiseCanExecuteChanged(); // break point here is never reached
};
}
private bool CanStart()
{
if (!String.IsNullOrEmpty(ConfigurationSettings.DownloadPath))
{
return true;
}
return false;
}
}
In my XAML I have a Start button and the with Command = "{Binding StartCommand}".
My ConfigurationSettings class just has a string for the DownloadPath which is bound to a textbox in the XAML:
public class ConfigurationSettings : BaseNotifyPropertyChanged
{
private string _downloadPath { get; set; }
public string DownloadPath
{
get { return _downloadPath; }
set
{
if (_downloadPath != value)
{
_downloadPath = value;
RaisePropertyChanged("DownloadPath"); // break point here IS reached
}
}
}
}
When the user enters a DownloadPath, I expect it to be triggering the PropertyChanged Event, and running my delegate method defined in the ViewModel constructor.
If I move the Command Button inside the ConfigurationSettings class I can do away with event subscription and just use StartCommand.RaiseCanExecuteChanged() right beneath RaisePropertyChanged("DownloadPath");. But I don't want the ICommand as part of my Model.
How can I trigger CanStart() when one of the properties of ConfigurationSettings changes?
UPDATE:
Here is the XAML for the text box binding:
<TextBlock Text="{Binding ConfigurationSettings.DownloadPath, Mode=TwoWay, UpdateSourceTrigger=LostFocus}" TextWrapping="WrapWithOverflow" />
And the button:
<Button Content="Start" Command="{Binding StartCommand}"></Button>
I should note that the bindings are working correctly. When I update the textblock, I can see in the ViewModel that ConfigurationSettings.DownloadPath is correctly being updated.
BaseNotifyPropertyChanged is an implementation of INotifyPropertyChanged like so:
public class BaseNotifyPropertyChanged : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string property)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}
}
I don't seem to be having any issues with the property changed event. I can put a break point in here and it is hit when I update the DownloadPath text box. It's when I subscribe to this PropertyChanged event in my ViewModel constructor, my delegate method isn't firing.
Hate to answer my own question but the people commenting made me think about restructuring my question - which led me to the answer before needing to make another update.
The solution was to move my event subscription inside the 'set' function for ConfigurationSettings:
private ConfigurationSettings _configurationSettings { get; set; }
public ConfigurationSettings ConfigurationSettings
{
get
{
return _configurationSettings;
}
set
{
if (_configurationSettings != value)
{
_configurationSettings = value;
_configurationSettings = new Model.ConfigurationSettings();
_configurationSettings.PropertyChanged += (o, args) =>
{
StartCommand.RaiseCanExecuteChanged();
};
RaisePropertyChanged("ConfigurationSettings");
}
}
}
The problem was where I was setting my Data Context which I did not originally suspect was at all the problem. I load the view model from an XML file on disk. And when the application is closed, I overwrite that file with the latest ViewModel.
In the constructor I was reading and setting the DataContext:
public MainWindowView()
{
InitializeComponent();
string appPath = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().CodeBase);
DataSourcePath = new Uri(System.IO.Path.Combine(appPath, DataFileName));
if (File.Exists(DataSourcePath.LocalPath))
{
XmlReader reader = XmlReader.Create(DataSourcePath.LocalPath);
DataContext = (MainWindowViewModel)serialize.Deserialize(reader);
reader.Close();
}
else
{
WriteDataViewModelToDisk(); // new empty view model written to disk
}
}
If this was the first time I ran the code, with no pre-existing file, my delegate event handler actually worked. The issue was when this code loaded a pre-existing XML file, it overwrote the ConfigurationSettings property in my view model - thus destroying the event subscription.
WPF is not normally my area, so I am a bit of a newbie, and I am having a bit of trouble figuring out how to achieve something in WPF which was a piece of cake in WinForms. I can't seem to find either the right thread in this forum or the right YouTube tutorial that leads me towards the answer. I am having problems getting a simple DataBinding to a WPF TextBox working correctly. The behaviour that I am trying to achieve is that any changes made to the TextBox are immediately reflected in the source class DataSet. It's a simple display/edit scenario and I'm sure there is a very simple answer.
This is how I would have done it in WinForms....
Form code:
public partial class Form1 : Form
{
private DATARECORD CURRENTUSER;
public Form1()
{
InitializeComponent();
CURRENTUSER = new DATARECORD(#"Data Source=C:\Users\rr187718\Documents\Personal\Programming\DynamicBackup\DynamicBackup\bin\Debug\Data\dbData.sdf");
CURRENTUSER.FncBind(CtlCopiesToKeep, "Value", "tblUser.CopiesToKeep");
}
//Test code to display the value in the DataSet
private void button1_Click(object sender, EventArgs e)
{
MessageBox.Show(CURRENTUSER.copiesToKeep.ToString());
}
}
Class code:
public class DATARECORD
{
private string ConnectionString;
private DataSet CurrentRecord;
public int copiesToKeep { get { return Int32.Parse(CurrentRecord.Tables["tblUser"].Rows[0]["CopiesToKeep"].ToString()); } }
public DATARECORD(string connectionString)
{
ConnectionString = connectionString;
CurrentRecord = new DataSet();
SQL SQL = new SQL(2);
DataTable userTable = SQL.fncSelectAsTable(ConnectionString, "tblUser", "USERID=2");
userTable.TableName = "tblUser";
CurrentRecord.Tables.Add(userTable);
userTable.Dispose();
}
public void FncBind(Control c, string type, string field)
{
c.DataBindings.Add(type, CurrentRecord, field, true, DataSourceUpdateMode.OnPropertyChanged);
}
}
I then just have simple TextBox on the main Form called "CtlCopiesToKeep" and a "test" button.
Does anyone know of a nice, simple, example that can show how to do this?
Many thanks in advance,
Dave
EDIT:
Hello Noel. Many thanks for taking the time to explain all that. I have put it altogether, but something seems to be wrong with the binding, because when I change the value in the TextBox it does not update the DataSet. Here is the code and the XAML. If anyone can point me in the right direction then it would be much appreciated.
UPDATED Main code
public partial class MainWindow : Window
{
public DATARECORD SELECTEDUSER;
private string ConnectionString = #"Data Source=C:\Users\rr187718\Documents\Personal\Programming\DynamicBackup\DynamicBackup\bin\Debug\Data\dbData.sdf";
public MainWindow()
{
InitializeComponent();
SELECTEDUSER = new DATARECORD(ConnectionString);
GrdMain.DataContext = SELECTEDUSER;
}
private void button1_Click(object sender, RoutedEventArgs e)
{
SELECTEDUSER.fncShowVals("BasePath");
}
}
UPDATED Class code
public class DATARECORD : INotifyPropertyChanged
{
private string ConnectionString;
private DataSet currentRecord = new DataSet();
private string BasePath = null;
public string basePath
{
get
{
return currentRecord.Tables["tblStorage"].Rows[0]["BasePath"].ToString() ;
}
set
{
BasePath = value;
OnPropertyChanged("BasePath");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public DATARECORD(string connectionString)
{
ConnectionString = connectionString;
SQL SQL = new SQL(ConnectionString, SQLVersion.CE);
DataTable storageTable = SQL.fncSelectAsTable(ConnectionString, "tblStorage", "USERID=2");
storageTable.TableName = "tblStorage";
currentRecord.Tables.Add(storageTable);
storageTable.Dispose();
}
public void fncShowVals(string test)
{
MessageBox.Show(currentRecord.Tables["tblStorage"].Rows[0][test].ToString());
}
protected void OnPropertyChanged(string value)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(value));
}
}
}
XAML for TextBox
<Window x:Class="WpfBind.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">
<Grid Name="GrdMain">
<TextBox Text="{Binding basePath, Mode=TwoWay, UpdateSourceTrigger =PropertyChanged}" Height="23" HorizontalAlignment="Left" Margin="124,70,0,0" Name="CtlBaseFolder" VerticalAlignment="Top" Width="120" />
<Label Content="BaseFolder" Height="28" HorizontalAlignment="Left" Margin="41,69,0,0" Name="label2" VerticalAlignment="Top" />
<Button Content="Button" Height="23" HorizontalAlignment="Left" Margin="263,142,0,0" Name="button1" VerticalAlignment="Top" Width="75" Click="button1_Click" />
</Grid>
UPDATE 02/04/2015
I now have this, but I don't understand how it references the DataSet? This code produces a blank textbox and if the value is changed it doesn't update the DataSet:
`private string ___basePath = null;
protected string _basePath
{
get
{
return ___basePath;
}
set
{
___basePath = value;
OnPropertyChanged("basePath");
}
}
public string basePath
{ //<- Bind to this property
get
{
return ___basePath;
}
set
{
_basePath = value;
}
}`
The underlying DataSet value is stored here:
currentRecord.Tables["tblStorage"].Rows[0]["BasePath"].ToString();
Many thanks in advance, Dave.
UPDATE - 02/04/2015 - 2
Hello Noel, I have applied your code, but it's still not working unfortunately (the DataSet does not reflect the changes in the TextBox if I click on the "test" button). Here is the whole code. I massively appreciate your time on this by the way, thanks so much!
public partial class MainWindow : Window
{
private string ConnectionString = #"Data Source=C:\Users\rr187718\Documents\Personal\Programming\DynamicBackup\DynamicBackup\bin\Debug\Data\dbData.sdf";
private readonly DATARECORD _data = null;
public DATARECORD Data
{
get
{
return _data;
}
}
public MainWindow()
{
InitializeComponent();
_data = new DATARECORD(ConnectionString);
DataContext = Data; //All controls connected to this class will now look for their value in 'Data' (DataContext inherits and must be a property because you can only bind to properties)
}
private void button1_Click(object sender, RoutedEventArgs e)
{
Data.fncShowVals("BasePath");
}
}
public class DATARECORD : INotifyPropertyChanged
{
private string ConnectionString;
private DataSet currentRecord = new DataSet();
private string ___basePath = null;
private string _basePath
{
get
{
if (___basePath == null)
{
//We only access the currentRecord if we did not yet stored the value
// otherwise it would read the currentRecord every time you type a char
// in the textbox.
// Also: Pay attention to multiple possible NullReferenceExceptions and IndexOutOfBoundsExceptions
___basePath = currentRecord.Tables["tblStorage"].Rows[0]["BasePath"].ToString();
}
return (___basePath == String.Empty) ? null : ___basePath;
}
set
{
___basePath = (value == null) ? String.Empty : value;
NotifyPropertyChanged("BasePath");
}
}
protected void PushBasePathToDataBase()
{
//Save the value of ___basePath to the database
}
public string BasePath
{ //The Binding recieves/sets the Data from/to this property
get
{
return _basePath;
}
set
{
_basePath = value;
}
}
public event PropertyChangedEventHandler PropertyChanged;
public DATARECORD(string connectionString)
{
ConnectionString = connectionString;
SQL SQL = new SQL(ConnectionString, SQLVersion.CE);
DataTable storageTable = SQL.fncSelectAsTable(ConnectionString, "tblStorage", "USERID=2");
storageTable.TableName = "tblStorage";
currentRecord.Tables.Add(storageTable);
storageTable.Dispose();
___basePath = currentRecord.Tables["tblStorage"].Rows[0]["BasePath"].ToString();
}
public void fncShowVals(string test)
{
MessageBox.Show(currentRecord.Tables["tblStorage"].Rows[0][test].ToString());
}
protected void NotifyPropertyChanged(string PropertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
}
}
It is great that you are using a binding to seperate data from the visuals. Since that was not really possible in winforms. In order for a binding to work you must do the following:
The textBox must have its DataContext set to the instance of a class which holds the binding-value. DataContext = MyDataInstance; You can set that on the textbox itself or on any parent.
The value as well as the DataContext you want to bind must be a public property. F.e:
private string _name = null;
public string Name{
get{
return _name;
}
set{
_name = value;
NotifyPropertyChanged("Name");
}
}
The Data Class must implement INotifyPropertyChanged
If that is all set up you can write your textbox in xaml:
<TextBlock Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
This Binding binds to the Name property of the instance specified in DataContext. It can retrieve the value from the property and it can write the data to it.
It recieves Data when you call NotifyPropertyChanged("Name"); in your DataClass
It writes Data when the property of the control changes (Requires Mode set to TwoWay and the UpdateSourceTrigger to PropertyChanged)
EDIT (regarding your additional content)
I noticed that you wanted to notify about your private field named "BasePath".
You must notify the property "basePath" and not the field behind it.
That is why I recommend a strict naming convention.
I do name private and protected fields like _privateOrProtected (1 underscore).
I name private or protected fields accessed by bindings properties like ___someData (3 underscores) and the binding property like SomeData. The reason is, that you usually don't want to set the private field directly except from the binding propertie's setter. Setting it directly would not call the NotifyPropertyChanged(); which obviously isn't what you want in almost all cases. And if you keep the 3 underscores throughout your app - everyone familliar with bindings should quickly understand the meaning.
For more complex data you might have a binding property accessing a private/protected property accessing a private field. I would solve it like this: SomeData, _someData, ___someData. You just have to make it clear wich properties or fields can be set in order to update the binding otherwise someone might change the value of ___someData and wonder why the binding isn't updating.
Since this is a quite important point in every WPF app I really want you to understand it. Here is an example for the stuff above:
private bool ___thisIsAwesome = true;
protected bool _thisIsAwesome{
get{
return ___thisIsAwesome;
}
set{
___thisIsAwesome = value;
NotifyPropertyChanged("ThisIsAwesome");
}
}
public bool ThisIsAwesome{ //<- Bind to this property
get{
return ___thisIsAwesome;
}
/*set{
_thisIsAwesome = value;
} NOTE: The setter is not accessable from outside of this class
because nobody can tell me that this is not awesome - it just is.
However I still want to be able to set the property correctly
from within my class (in case I change my mind), that is why I
added the protected property.
If you omit a getter/setter like this one make sure your
<br>Binding Mode</b> does not try to access the omited accessors.
Also check the output window too find possible binding errors
which never throw exceptions.
*/
}
In this code you should now recognize that setting ThisIsAwesome and _thisIsAwesome will both update the binding. But beware of setting ___thisIsAwesome because it won't update the Binding. The setter of ThisIsAwesome is currently not available (whatever reason) and that's why I added the protected property. Do you understand what I want to achieve with that?
EDIT2 (because your code still doesn't work)
public partial class MainWindow : Window {
private readonly MyData _data = null;
public MyData Data{
get{
return _data;
}
}
public MainWindow() {
_data = new MyData();
DataContext = Data; //All controls connected to this class will now look for their value in 'Data' (DataContext inherits and must be a property because you can only bind to properties)
}
}
public class MyData : INotifyPropertyChanged {
private string ___basePath = null;
private string _basePath {
get {
if (___basePath == null) {
//We only access the currentRecord if we did not yet stored the value
// otherwise it would read the currentRecord every time you type a char
// in the textbox.
// Also: Pay attention to multiple possible NullReferenceExceptions and IndexOutOfBoundsExceptions
___basePath = currentRecord.Tables["tblStorage"].Rows[0]["BasePath"].ToString();
}
return (___basePath == String.Empty) ? null : ___basePath;
}
set {
___basePath = (value == null) ? String.Empty : value;
NotifyPropertyChanged("BasePath");
}
}
protected void PushBasePathToDataBase() {
//Save the value of ___basePath to the database
}
public string BasePath{ //The Binding recieves/sets the Data from/to this property
get{
return _basePath;
}
set{
_basePath = value;
}
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string PropertyName){
if(PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
}
#endregion INotifyPropertyChanged
}
And finally the textbox in your MainWindow's xaml:
<TextBlock Text="{Binding BasePath, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
A newbie question, but I can't get it to work. In trying to use the answer to SO "How Should the View Model Close the form"
, I have a UserControl defined with:
<UserControl
.....
h:DialogCloser.DialogResult="{Binding DialogResult}"
>
which while designing shows:
Property 'DialogResult' is not attachable to elements of type 'UserControl'.
The DialogCloser is defined as:
public static class DialogCloser
{
public static readonly DependencyProperty DialogResultProperty =
DependencyProperty.RegisterAttached(
"DialogResult",
typeof(bool?),
typeof(DialogCloser),
new PropertyMetadata(DialogResultChanged));
private static void DialogResultChanged(
DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var window = d as Window;
if (window != null)
window.DialogResult = e.NewValue as bool?;
}
public static void SetDialogResult(Window target, bool? value)
{
target.SetValue(DialogResultProperty, value);
}
}
The UserControl is opened by:
var win = new WindowDialog();
win.Title = title;
win.DataContext = datacontext;
return win.ShowDialog();
In the View Model for the UserControl, I have:
public new void DoExit()
{
DialogResult = true;
}
private bool? dialogresult;
public bool? DialogResult
{
get
{
return dialogresult;
}
set
{
if (dialogresult != value)
{
dialogresult = value;
OnPropertyChanged("DialogResult");
}
}
}
When using DoExit(), the modal dialog, my UserControl, does not close. What's wrong? And how do I get the designer (VS 2010) to not throw error?
Thanks is advance for any help.
Addendum:
public class ModalDialogService : IModalDialogService
{
public bool? ShowDialog(string title, object datacontext)
{
var win = new WindowDialog();
win.Title = title;
win.DataContext = datacontext;
return win.ShowDialog();
}
}
Note: If the "UserControl" is rather made as a "Window", the designer is happy, but then the error is:
"Window must be the root of the tree. Cannot add Window as a child of Visual."
Help somebody?
I am new to this, but putting everything together, I would suggest this as a possible answer for newbies like myself.
Does this comply with best MVVM practices?
First, as the designer stated "Property 'DialogResult' is not attachable to elements of type 'UserControl'.
My translation, a "control" is not a window--so it can't be closed. Therefore,
h:DialogCloser.DialogResult="{Binding DialogResult}"
is wrong. Remove it.
Second, my usercontrol is being displayed as the sole element of an otherwise plain window as
<Window x:Class="Nova5.UI.Views.Ink.WindowDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="WindowDialog"
WindowStyle="SingleBorderWindow"
WindowStartupLocation="CenterOwner" SizeToContent="WidthAndHeight">
<ContentPresenter x:Name="DialogPresenter" Content="{Binding .}"/>
</Window>
Now, the whole thing is initiated when the top most view model does
var dialog = new WindowDialog
{
Title = "It's me Margaret",
ShowInTaskbar = false,
Topmost = true,
ResizeMode = ResizeMode.NoResize
};
dialog.Owner = Application.Current.Windows.OfType<Window>().SingleOrDefault(x => x.IsActive);
ModalDialogService dialogService = new ModalDialogService();
dialogService.ShowDialog<PrescriptionWriterViewModel>(dialog,
new PrescriptionWriterViewModel(),
returnedViewModelInstance =>
{
if (dialog.DialogResult.HasValue && dialog.DialogResult.Value)
{
}
}
);
};
When Exit is clicked on the usercontrol, it performs by way of a DelegateCommand in its view model
public new void DoExit()
{
OnRequestClose();
}
public event EventHandler RequestClose;
void OnRequestClose()
{
EventHandler handler = this.RequestClose;
if (handler != null)
{
handler(this, EventArgs.Empty);
}
}
This request to close is then handled in the initiating dialog service,
public void ShowDialog<TDialogViewModel>(IModalWindow view, TDialogViewModel viewModel, Action<TDialogViewModel> onDialogClose)
{
view.DataContext = viewModel;
IDialogViewModel vm = viewModel as IDialogViewModel;
if (vm !=null)
vm.RequestClose += (s,e)=> view.Close(); //RequestClose is the event in the ViewModel that the view will listen for.
if (onDialogClose != null)
{
view.Closed += (sender, e) => onDialogClose(viewModel);
}
view.Show();
}
When the view is closed, the onDialogClose(viewModel), then calls back to the topmost view model and completes the action specified with
returnedViewModelInstance =>
{
if (dialog.DialogResult.HasValue && dialog.DialogResult.Value)
{
}
}
So completes several intense hours of learning. I hope it is of use to somebody.
I have a static collection of items (say numbers from 1 to 100) that I'm presenting in a ComboBox. I can bind these items to the ComboBox no problem.
My question is how to bind a second ComboBox to a subset of those items. The behavior I want is to have the second ComboBox bound to the subset of items remaining after the first ComboBox is selected. For example the first ComboBox would show 1,2,3...,100. If the number 43 is selected in the first ComboBox then the second ComboBox should show 44,45,...,100.
How can this be accomplished and have the second ComboBox update if the first is changed without a lot of code-behind?
I would do this with a using MVVM pattern.
Create a class that implements INotifyChange and expose three Property.
ICollection FullCollection
int FirstIndex
ICollection PartialCollection
Use this class as DataContext for your Control and bind SelectedIndex of the first combo box to FirstIndex property, ItemSource of first combobox to FullCollection and ItemSource of second collection to PartialCollection (Be sure that SelectedIndex binding mode is Two Way).
Then on set of FirstIndex property set the PartialCollection property as you want.
Remember that you have to use NotifyPropertyChange on set method of each Properties.
Hope this help.
I'd go with MVVM design but if you want to just do testing and want to see quick results without going into design patterns then I'd suggest using something like CLINQ / BLINQ / Obtics framework to leverege the power of LINQ while keeping the results live for the combo box.
Since LoSciamano has already posted a reply on that (while i was posting this!) I won't dive into details of MVVM implementation
I would expose the first collection as an ObservableCollection<T> and the second collection as a bare IEnumerable. You can then use the default collection view to handle the events necessary to re-filter the sub collection.
class FilteredVM : ViewModelBase
{
public ObservableCollection<MyVM> Items { get; private set; }
public IEnumerable<MyVM> SubItems { get; private set; }
public FilteredVM()
{
this.Items = new ObservableCollection<MyVM>();
this.SubItems = Enumerable.Empty<MyVM>();
var view = CollectionViewSource.GetDefaultView(this.Items);
view.CurrentChanged += (sender, e) => { SetupFilter(); };
view.CollectionChanged += (sender, e) => { SetupFilter(); };
}
private void SetupFilter()
{
var view = CollectionViewSource.GetDefaultView(this.Items);
var current = view.CurrentItem;
if (current != null)
{
this.SubItems = this.Items.Where((vm,idx) => idx > view.CurrentPosition);
}
else
{
this.SubItems = Enumerable.Empty<MyVM>();
}
this.OnPropertyChanged("SubItems");
}
}
Alternatively, if you'd like to keep CollectionViewSource out of your VM:
class FilteredVM : ViewModelBase
{
private MyVM selectedItem;
public MyVM SelectedItem
{
get { return this.selectedItem; }
set
{
if (value != this.selectedItem)
{
this.selectedItem = value;
this.OnPropertyChanged("SelectedItem");
this.SetupFilter();
}
}
}
public ObservableCollection<MyVM> Items { get; private set; }
public IEnumerable<MyVM> SubItems { get; private set; }
public FilteredVM()
{
this.Items = new ObservableCollection<MyVM>();
this.SubItems = Enumerable.Empty<MyVM>();
this.Items.CollectionChanged += (sender, e) => { this.SetupFilter(); };
}
private void SetupFilter()
{
if (this.SelectedItem != null)
{
var item = this.SelectedItem; // save for closure
this.SubItems = this.Items.SkipWhile(vm => vm != item).Skip(1);
}
else
{
this.SubItems = Enumerable.Empty<MyVM>();
}
this.OnPropertyChanged("SubItems");
}
}
Keep in mind this will require SelectedItem to be properly bound in the View to the ViewModel. The first approach listed allows the SelectedItem to be bound to anywhere (or nowhere).
Have 2 Observable collections so that when item in the 1st box is selected, it will kick off method which clears and re populates the 2nd collection. As it is observableCollection, it gets reflected in the WPF GUI automatically
Here is a concrete example (as usual, may be improved, but the idea is here) :
Code behind an view model :
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
DataContext = new ViewModel();
}
}
public class ViewModel : INotifyPropertyChanged
{
public ViewModel()
{
Initialsource = new ObservableCollection<int>();
for (int i = 0; i < 101; i++)
{
Initialsource.Add(i);
}
}
private int _selectedsourceItem;
public int SelectedsourceItem
{
get { return _selectedsourceItem; }
set
{
_selectedsourceItem = value;
SubsetSource = new ObservableCollection<int>(Initialsource.Where(p => p > _selectedsourceItem));
InvokePropertyChanged(new PropertyChangedEventArgs("SubsetSource"));
InvokePropertyChanged(new PropertyChangedEventArgs("SelectedsourceItem"));
}
}
private ObservableCollection<int> _initialsource;
public ObservableCollection<int> Initialsource
{
get { return _initialsource; }
set
{
_initialsource = value;
InvokePropertyChanged(new PropertyChangedEventArgs("Initialsource"));
}
}
private ObservableCollection<int> _subsetSource;
public ObservableCollection<int> SubsetSource
{
get { return _subsetSource ?? (_subsetSource = new ObservableCollection<int>()); }
set
{
_subsetSource = value;
InvokePropertyChanged(new PropertyChangedEventArgs("SubsetSource"));
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void InvokePropertyChanged(PropertyChangedEventArgs e)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, e);
}
}
XAML :
<Grid HorizontalAlignment="Center" VerticalAlignment="Center">
<StackPanel Orientation="Horizontal">
<ComboBox Width="100" ItemsSource="{Binding Initialsource}" SelectedItem="{Binding SelectedsourceItem, Mode=TwoWay}"></ComboBox>
<ComboBox Width="100" ItemsSource="{Binding SubsetSource}"></ComboBox>
</StackPanel>
</Grid>
You can use my ObservableComputations library:
ObservableCollection<Item> itemsSubset = ItemsObservableCollection
.Skiping(fromIndex)
.Taking(itemsInSubsetCount);
itemsSubset reflacts all the changes in ItemsObservableCollection.
Scenario: In a Silverlight 4 MVVM project, we have a ListBox control containing items, the selected item is two-way-bound to the appropriate property in the ViewModel. Another control (for example reasons, I've stripped it down to a single TextBox) is data bound to the selected item's content. The value should update on leave/focus lost.
Problem: When the value in the TextBox is changed and we leave that TextBox by pressing the Tab key, everything works as desired - the value is updated. However, if the user clicks on a different item in the ListBox, then the SelectedItem setter is fired before the content of TextBox setter is fired, leaving no chance to handle the user input.
You can see in debugger, when adding breakpoints to the property setters, that the new ListView selection is applied first, before the TextBox update is processed.
Desired behavior: We need to know that the currently selected item was modified before the user has selected another item. It's not desired to have a custom update trigger which would notify on each key press (we know that's possible).
Can you help?
Code (a very simple example):
ViewModel
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
public class ItemViewModel : ViewModelBase
{
private string _content;
public ItemViewModel(string initContent)
{
_content = initContent;
}
public string Content
{
get
{
return _content;
}
set
{
if (_content != value)
{
_content = value;
OnPropertyChanged("Content");
}
}
}
}
public class MainViewModel : ViewModelBase
{
private ObservableCollection<ItemViewModel> _items =
new ObservableCollection<ItemViewModel>();
private ItemViewModel _selectedViewModel;
public ObservableCollection<ItemViewModel> Items
{
get
{
return _items;
}
}
public ItemViewModel SelectedItem
{
get
{
return _selectedViewModel;
}
set
{
if (_selectedViewModel != value)
{
_selectedViewModel = value;
OnPropertyChanged("SelectedItem");
}
}
}
}
XAML
<Grid x:Name="LayoutRoot" Background="White">
<ListBox Height="100"
HorizontalAlignment="Left"
Margin="12,12,0,0"
VerticalAlignment="Top"
ItemsSource="{Binding Items}"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
DisplayMemberPath="Content"
Width="220" />
<TextBox Height="23"
HorizontalAlignment="Left"
Margin="12,118,0,0"
Text="{Binding SelectedItem.Content, Mode=TwoWay}"
VerticalAlignment="Top"
Width="220" />
</Grid>
XAML Code Behind
public MvvmTestView()
{
InitializeComponent();
Loaded += new RoutedEventHandler(MvvmTestView_Loaded);
}
void MvvmTestView_Loaded(object sender, RoutedEventArgs e)
{
MainViewModel viewModel = new MainViewModel();
viewModel.Items.Add(new ItemViewModel("Hello StackOverflow"));
viewModel.Items.Add(new ItemViewModel("Thanks to Community"));
DataContext = viewModel;
}
UPDATE 1
I present a self designed solution for you to check out, which will be probably be the accepted one, I still want to encourage you to make comments and give your hints. Thanks.
You could add a behavior to your textbox to updated the binding every time the text is changed in the textbox. Maybe this solved your problems.
Here´s the code for the Behavior class:
public class UpdateTextBindingOnPropertyChanged : Behavior<TextBox> {
// Fields
private BindingExpression expression;
// Methods
protected override void OnAttached() {
base.OnAttached();
this.expression = base.AssociatedObject.GetBindingExpression(TextBox.TextProperty);
base.AssociatedObject.TextChanged+= OnTextChanged;
}
protected override void OnDetaching() {
base.OnDetaching();
base.AssociatedObject.TextChanged-= OnTextChanged;
this.expression = null;
}
private void OnTextChanged(object sender, EventArgs args) {
this.expression.UpdateSource();
}
}
Heres the XAML:
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:local="Namespace of the class where UpdateTextBindingOnPropertyChanged is defined"
<TextBox Text="{Binding SelectedItem.Content, Mode=TwoWay}">
<i:Interaction.Behaviors>
<local:UpdateTextBindingOnPropertyChanged />
</i:Interaction.Behaviors>
</TextBox >
This is one solution we currently came up with. It has the advantage that it separates different tasks to the appropriate layer. For example, the View enforces an update of the binding, while the ViewModel tells the View to do so. Another advantage is that its handled synchronously, which would for example allow to check the content right before switching away, and the call-stack remains unchanged without raising "External Code" (Going over Dispatcher or even DispatcherTimer would do so) which is better for maintenance and flow control. A disadvantage is the new Event which has to be bound and handled (and finally unbound. I present an anonymous handler only for example reasons).
How to get there?
In ViewModelBase, implement a new ForceBindingUpdate event:
public abstract class ViewModelBase : INotifyPropertyChanged
{
// ----- leave everything from original code ------
public event EventHandler ForceBindingUpdate;
protected void OnForceBindingUpdate()
{
var handler = ForceBindingUpdate;
if (handler != null)
handler(this, EventArgs.Empty);
}
}
In MainViewModel, update the setter of the SelectedItem property:
set // of SelectedItem Property
{
if (_selectedViewModel != value)
{
// Ensure Data Update - the new part
OnForceBindingUpdate();
// Old stuff
_selectedViewModel = value;
OnPropertyChanged("SelectedItem");
}
}
Update the MvvmTestView Code Behind to implement the new event:
void MvvmTestView_Loaded(object sender, RoutedEventArgs e)
{
// remains unchanged
Mvvm.MainViewModel viewModel = new Mvvm.MainViewModel();
viewModel.Items.Add(new Mvvm.ItemViewModel("Hello StackOverflow"));
viewModel.Items.Add(new Mvvm.ItemViewModel("Thanks to Community"));
// Ensure Data Update by rebinding the content property - the new part
viewModel.ForceBindingUpdate += (s, a) =>
{
var expr = ContentTextBox.GetBindingExpression(TextBox.TextProperty);
expr.UpdateSource();
};
// remains unchanged
DataContext = viewModel;
}
Last but not least, the minimal XAML Update: Give the TextBox a name by adding x:Name="ContentTextBox" Attribute to the TextBoxs XAML.
Done.
Actually, I don't know if this is the cleanest solution, but it gets close to what we had in mind.
Maybe you could handle TextBox LostFocus then (instead of listening to every key press)?
Other idea would be to keep a proxy property on the ViewModel instead of directly binding to SelectedItem.Content and writing some code to make sure the item is updated.
Solution №1
public class LazyTextBox: TextBox
{
//bind to that property instead..
public string LazyText
{
get { return (string)GetValue(LazyTextProperty); }
set { SetValue(LazyTextProperty, value); }
}
public static readonly DependencyProperty LazyTextProperty =
DependencyProperty.Register("LazyText", typeof(string), typeof(LazyTextBox),
new PropertyMetadata(null));
//call this method when it's really nessasary...
public void EnsureThatLazyTextEqualText()
{
if (this.Text != this.LazyText)
{
this.LazyText = this.Text;
}
}
}
Solution №2 (works as magic :) )
public class MainViewModel : ViewModelBase
{
private ObservableCollection<ItemViewModel> _items =
new ObservableCollection<ItemViewModel>();
private ItemViewModel _selectedViewModel;
public ObservableCollection<ItemViewModel> Items { get { return _items; } }
public ItemViewModel SelectedItem
{
get { return _selectedViewModel; }
set
{
if (_selectedViewModel != value)
{
if (SelectedItem != null)
{
SelectedItem.Content = SelectedItem.Content;
}
_selectedViewModel = value;
// A little delay make no harm :)
var t = new DispatcherTimer();
t.Interval = TimeSpan.FromSeconds(0.1);
t.Tick += new EventHandler(t_Tick);
t.Start();
}
}
}
void t_Tick(object sender, EventArgs e)
{
OnPropertyChanged("SelectedItem");
(sender as DispatcherTimer).Stop();
}
}
I know that in MVVM we do not want to put code in code behind. But in this instance it hurts nothing as it is entirely maintained in the UI and SOP is maintained.
By putting a ghost element to take focus we can swap the focus back in forth forcing
the text box to commit its contents. So in the code behind we take care of the focus wiggle.
But yet we still are using a relay command Update Command to execute the save. So the order is good as the Click event fires wiggling the view. And then the relay command UpdateCommand will fire and the textbox is committed and ready for update.
<MenuItem Header="_Save"
Command="{Binding UpdateCommand}" Click="MenuItem_Click">
</MenuItem>
private void MenuItem_Click(object sender, RoutedEventArgs e)
{
UIElement elem = Keyboard.FocusedElement as UIElement;
Keyboard.Focus(ghost);
Keyboard.Focus(elem);
}
Solution #3
public abstract class ViewModelBase : INotifyPropertyChanged
{
private List<string> _propNameList = new List<string>();
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
_propNameList.Add(propertyName);
var t = new DispatcherTimer();
t.Interval = TimeSpan.FromSeconds(0);
t.Tick += new EventHandler(t_Tick);
t.Start();
}
void t_Tick(object sender, EventArgs e)
{
if (_propNameList.Count > 0)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(_propNameList[0]));
_propNameList.Remove(_propNameList[0]);
}
}
}
PS: it's the same timer.. but this solution is more generic..