WPF how to transfer data between windows (MVVM)? - wpf

I know there are a lot of similar questions and I spent two hours by now trying to implementing them but can't proceed. So the problem seems simple. When I don't have a viewmodel, I can set the datacontext to a class and it is very easy to transfer data with that class. But when there is viewmodel, I have to set the datacontext to that and can't find a way to return any value after that. I tried to implement countless solutions to the problem but it seems that they are above my skill level. Thank you so much for your help!
The important parts of my code (its a simple game which i want to save, where save is named by userinput) The first window, where I want to get data from the second window
case Key.Escape: {
Thread t = new Thread(() => {
SaveGameWindow pw = new SaveGameWindow(); //the second window
if ((pw.ShowDialog() == true) && (pw.Name != string.Empty)) //pw.Name always empty
{
ILogicSaveGame l = new LogicSaveGame();
l.Write(pw.Name, "saved_games.txt");
MessageBox.Show("game saved");
}
});
t.SetApartmentState(ApartmentState.STA);
t.Start();
XAML (from now on everything belongs to the SaveGameWindow):
<Window.Resources>
<local:SaveGameViewModel x:Key="my_viewmodel"/>
</Window.Resources>
<Grid DataContext="{StaticResource my_viewmodel}">
<TextBox Text="{Binding Name}"/> //i want to acces this in the first window
<Button Command="{Binding CloseCommand}"
Content="Save"/>
Code behind:
private readonly SaveGameViewModel vm;
public SaveGameWindow()
{
this.InitializeComponent();
this.vm = this.FindResource("my_viewmodel") as SaveGameViewModel;
if (this.vm.CloseAction == null)
{
this.vm.CloseAction = new Action(() => this.Close());
}
}
Viewmodel
public class SaveGameViewModel : ViewModelBase
{
public SaveGameViewModel()
{
this.CloseCommand = new RelayCommand(() => this.Close());
}
public string Name { get; set; }
public ICommand CloseCommand { get; private set; }
public Action CloseAction { get; set; }
private void Close()
{
this.CloseAction();
}
}
I use galasoft mvvmlightlibs

There are many solutions to this problem. The simplest solution is to use a shared view model for both windows and data binding. Since both windows would share the same DataContext, both have access to the same data or model instance by simply referencing their DataContext property.
But if you prefer to have individual view models, you would choose a different solution.
Solution 1
If you want to use a dedicated view model for each window, you can always use composition and make e.g. an instance SaveGameViewModel a member of MainWindowViewModel. Any class that has access to MainWindowViewModel will also have access to the SaveGameViewModel and its API, either directly or via delegating properties.
This example uses direct access by exposing SaveGameViewModel as a public property of MainWindowViewModel:
SaveGameViewModel.cs
public class SaveGameViewModel : INotifyPropertyChanged
{
private string name;
public string Name
{
get => this.name;
set
{
this.name = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
MainWindowViewModel.cs
public class MainWindowViewModel : INotifyPropertyChanged
{
public SaveGameViewModel SaveGameViewModel { get; set; }
// Allow to create an instance using XAML
public MainWindowViewModel() {}
// Allow to create an instance using C#
public MainWindowViewModel(SaveGameViewModel saveGameViewModel)
=> this.SaveGameViewModel = saveGameViewModel;
}
App.xaml
<Application>
<Application.Resources>
<MainWindowViewModel x:Key="MainWindowViewModel">
<MainWindowViewModel.SaveGameViewModel>
<SaveGameViewModel />
</MainWindowViewModel.SaveGameViewModel>
</MainWindowViewModel>
</Application.Resources>
</Application>
SaveGameWindow.xaml
<Window DataContext="{Binding Source={StaticResource MainWindowViewModel}, Path=SaveGameViewModel}">
<TextBox Text="{Binding Name}" />
<Window>
MainWindow.xaml
<Window DataContext="{StaticResource MainWindowViewModel}">
<Window>
MainWindow.xaml.cs
partial class MainWindow : Window
{
private void OnKeyPressed(object sender, KeyEventArgs e)
{
if (e.Key == Key.Escape)
{
var mainWindowViewModel = this.DataContext as MainWindowViewModel;
string saveGameName = mainWindowViewModel.SaveGameViewModel.Name;
}
}
}
Solution 2
Since you are just showing a dialog, you can store the current instance of the SaveGameViewModel or its values of interest after the dialog has been closed:
MainWindow.xaml.cs
partial class MainWindow : Window
{
private SaveGameViewModel CurrentSaveGameViewModel { get; set; }
private bool IsSaveGameValid { get; set; }
private void ShowDialog_OnSaveButtonClick(object sender, RoutedEventArgs e)
{
var saveGameDialog = new SaveGameWindow();
this.IsSaveGameValid = saveGameDialog.ShowDialog ?? false;
this.CurrentSaveGameViewModel = saveGameDialog.DataContext as SaveGameViewModel;
}
private void OnKeyPressed(object sender, KeyEventArgs e)
{
if (e.Key == Key.Escape && this.IsSaveGameValid)
{
string saveGameName = this.CurrentSaveGameViewModel.Name;
}
}
}
MainWindow.xaml
<Window>
<Window.DataContext>
<MainWindowViewModel />
</Window.DataContext>
<Window>
SaveGameWindow.xaml
<Window>
<Window.DataContext>
<SaveGameViewModel />
</Window.DataContext>
<TextBox Text="{Binding Name}" />
<Window>

Related

PropertyChanged remains null even if the property has been changed

I am struggling with this for a while and I cannot figure it out. I have a button and a textBox. The textBox is linked to a property named: MessageDisplay. I want to be able to access this property and update the textBox in several places. Sadly, the PropertyChanged is null. The weird thing is that if I copy/paste the MessageDisplayModel class into the *MessageViewModel * class, it works ...
here is my code :
XAMLfile :
<Grid>
<Button Command="{Binding DisplayTextCommand}" Name="DisplayTextCommand" Margin="53,72,544.6,286" Width="Auto">Push</Button>
<TextBox Name="MessageDisplay" Text="{Binding MessageDisplay, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" />
</Grid>
MessageDisplayModel file
public class MessageDisplayModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _message;
public string MessageDisplay
{
get { return _message; }
set
{
this._message = value;
this.OnPropertyChanged("MessageDisplay");
}
}
public void UpdateTextBox(string output)
{
MessageDisplay = output;
}
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
{
var e = new PropertyChangedEventArgs(propertyName);
handler(this, e);
}
}
}//class
MessageViewModel file:
public class MessageViewModel
{
private ICommand _testCommand;
public MessageDisplayModel MessageDisplaySmt = new MessageDisplayModel();
public ICommand DisplayTextCommand
{
get
{
return new DelegateCommand(DisplayMessage);
}
set
{
if (_testCommand == value) return;
_testCommand = value;
}
}
public void DisplayMessage()
{
MessageDisplaySmt.UpdateTextBox("Successfuly downloaded");
}
}//class
MainWindow file
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
MessageDisplay.DataContext = new MessageDisplayModel();
DisplayTextCommand.DataContext = new MessageViewModel();
}
}//class
I update the MessageDisplay property by using the method UpdateTextBox(string). I call this method on the click of the button. When debugging the property gets updated but when time comes to notify the UI that the property has changed, PropertyChangedEventHandler PropertyChanged has its value null ... But if I write something in the textBox, the PropertyChangedEventHandler PropertyChanged gets changed and isn't null anymore. All I want is to be able to change the textBox's property whenever I want and from anywhere I want to.
Thank you
You are using two different instances of MessageDisplayModel. You must use a shared instance.
Also the DisplayTextCommand is implemented "wrong". The set method is redundant as the property's get always returns a new instance of the ICommand.
MessageViewModel.cs
public class MessageViewModel
{
pulic MessageViewModel()
{
}
pulic MessageViewModel(MessageDisplayViewModel messageDisplayViewModel)
{
this.MessageDisplaySmt = messageDisplayViewModel;
}
public void DisplayMessage()
{
this.MessageDisplaySmt.UpdateTextBox("Successfuly downloaded");
}
public MessageDisplayViewModel MessageDisplaySmt { get; set; }
public ICommand DisplayTextCommand { get => new DelegateCommand(DisplayMessage); }
}
MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
// Alternatively use XAML to set the DataContext (see MainWindow.xaml). Would require a parameterless constructor.
this.DataContext = new MessageViewModel(new MessageDisplayViewModel());
}
}
MainWindow.xaml
<Window>
<!--
Alternative DataContext declaration using XAML instead of C#.
Requires a parameterless constructor for both view model objects.
-->
<Window.DataContext>
<MessageViewModel>
<MessageViewModel.MessageDisplaySmt>
<MessageDisplayViewModel />
</MessageViewModel.MessageDisplaySmt>
</MessageViewModel>
</Window.DataContext>
<StackPanel>
<Button Command="{Binding DisplayTextCommand}"
Content="Push" />
<TextBox Text="{Binding MessageDisplaySmt.MessageDisplay}" />
</StackPanel>
</Window>

resolve specific dependency in unity DI/IOC

While exploring DI/IOC with Unity with WPF, I came across a question and need your feedback. Please consider the following scenario...
================================================================
public interface IDataServices
{
string GetData();
}
================================================================
public class CopyTextDataServices : IDataServices
{
public string GetData()
{
return "copy text from CopyTextDataServices";
}
}
================================================================
public class TextDataServices : IDataServices
{
public string GetData()
{
return "I am injected by setter property injection";
}
}
================================================================
public interface ITextViewModel
{
string LabelContnet { get; set; }
}
================================================================
public class TextViewModel : ITextViewModel
{
public TextViewModel()
{
LabelContnet = "This is from view model";
}
public string LabelContnet { get; set; }
}
================================================================
public partial class MainWindow : Window
{
public MainWindow(ITextViewModel textViewModel)
{
InitializeComponent();
Loaded += MainWindow_Loaded;
DataContext = textViewModel;
}
[Dependency]
public IDataServices Services { get; set; }
containing the event data.</param>
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
LabelLeft.Content = Services.GetData();
}
}
================================================================
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
IUnityContainer container = new UnityContainer();
container.RegisterType<IDataServices, TextDataServices>();
container.RegisterType<IDataServices, CopyTextDataServices>();
container.RegisterType<ITextViewModel, TextViewModel>();
var window = container.Resolve<MainWindow>();
window.Show();
}
}
================================================================
<Window x:Class="TestAppWPF.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" FontSize="20">
<StackPanel>
<Label Content="{Binding Path=LabelContnet,FallbackValue=Left}" HorizontalAlignment="Left" Name="LabelLeft" />
<Label Content="{Binding Path=LabelContnet,FallbackValue=Right}" HorizontalAlignment="Left" Name="LabelRight" />
</StackPanel>
</Window>
===================================================================
Now the result of this appears in the labels is
copy text from CopyTextDataServices
This is from view model
But I want to know if I want to get data from TextDataServices, how can I do that?
The problem is in this line:
container.RegisterType<IDataServices, TextDataServices>();
// This overwrites the previous mapping.
// All dependencies to IDataServices will use CopyTextDataServices.
container.RegisterType<IDataServices, CopyTextDataServices>();
If you want to have both IDataServices, you'll need to register one or both as named instances.
container.RegisterType<IDataServices, TextDataServices>("TextDataServicesName");
container.RegisterType<IDataServices, CopyTextDataServices>("CopyTextDataServicesName");
In your control:
[Dependency("TextDataServicesName")]
public IDataServices Services { get; set; }

WPF DataContext updated but UI not updated

I have maximum simple app. I want to fill listbox when button pressed. I use binding, window DataContext is updated after some operation, but UI not updated!
Here is the code:
MainWindow.xaml
<Window x:Class="WpfApplication1.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>
<Button Content="Button" HorizontalAlignment="Left" Margin="432,288.04,0,0" VerticalAlignment="Top" Width="75" Click="Button_Click"/>
<ListBox x:Name="urlsListBox" ItemsSource="{Binding Urls}" HorizontalAlignment="Left" Height="300" Margin="10,10,0,0" VerticalAlignment="Top" Width="417"/>
</Grid>
MainWindow.xaml.cs
namespace WpfApplication1
{
public partial class MainWindow : Window
{
ViewModel model = new ViewModel();
public MainWindow()
{
InitializeComponent();
this.DataContext = model;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
model.GetUrls();
}
}
}
ViewModel.cs
namespace WpfApplication1
{
class ViewModel
{
private ObservableCollection<Url> Urls { get; set; }
public ViewModel()
{
Urls = new ObservableCollection<Url>();
}
public void GetUrls()
{
for (int i = 0; i < 5; i++)
{
Urls.Add(new Url { link = i.ToString() });
}
}
}
public class Url
{
public string link { get; set; }
}
}
Problem stems from the Urls property within the ViewModel class. You need to make Urls public, otherwise the MainWindow cannot access the property:
ViewModel:
namespace WpfApplication1
{
public class ViewModel
{
//make this public otherwise MainWindow will not have access to it!
public ObservableCollection<Url> Urls { get; set; }
public ViewModel()
{
Urls = new ObservableCollection<Url>();
}
public void GetUrls()
{
for (int i = 0; i < 5; i++)
{
Urls.Add(new Url { link = i.ToString() });
}
}
}
public class Url
{
public string link { get; set; }
}
}
Hope this helps and let me know if you have any questions!
You need to support property change notification. Use the NuGet package manager to reference the MVVM Lite project, derive your ViewModel from ViewModelBase and then change your link property to this:
private string _link;
{
public string link
{
get {return this._link;}
set {this._link=value; RaisePropertyChanged(() => this.link); }
}
}
You'll also need to do this for your URLs property which needs to be public in order for binding to work. Also I know this is a little bit outside the scope of the question but in general you shouldn't use the Click handler like this, the "proper" way is to add a RelayCommand to your ViewModel and bind your button's Command property to that.

How do I create a new instance of a control in the view without violating MVVM

I am encountering a similar problem to what is described in this SO question. The suggested solution is to create a new WebBrowser Control for each now page (PDF) we wish to present (Overwriting the old WebBrowser control).
What is the correct way of creating a new control like that in MVVM? I trying to keep the VM ignorant about the implementation of the view.
Why does the VM need to know? Why can't the view just hook into an appropriate event (define one if you like, or just use the PropertyChanged) and recreate the control?
Create an interface in the ViewModel named IBrowserCreator, with a method called CreateBrowser().
Create a static class in the ViewModel named ViewHelper, and add to it a static property of type IBrowserCreator named BrowserCreator.
In the View layer, create a new class called BrowserCreator, which implements ViewModel.IBrowserCreator.
In the View initialization code, instantiate a BrowserCreator, and assign it to ViewModel.ViewHelper.BrowserCreator.
From your ViewModel, you should now be able to call:
ViewHelper.BrowserCreator.CreateBrowser()
Obviously this answer is a framework only, but it should give you the general idea. You'll need to implement the CreateBrowser method to suit your exact needs.
why not simply use a Datatemplate and let WPF do the rest?
create a usercontrol with the webbrowser. you have to add an attached property because you can not bind to source directly.
<UserControl x:Class="WpfBrowser.BrowserControl"
xmlns:WpfBrowser="clr-namespace:WpfBrowser" >
<Grid>
<WebBrowser WpfBrowser:WebBrowserUtility.BindableSource="{Binding MyPdf}"/>
</Grid>
</UserControl>
create a viewmodel which handle your uri
public class MyPdfVM
{
public Uri MyPdf { get; set; }
public MyPdfVM()
{
this.MyPdf = new Uri(#"mypdf path");
}
}
take your pageviewmodel, add the pdfviewmodel and take a contentcontrol in your view
public class MyPageViewmodel: INotifyPropertyChanged
{
private MyPdfVM _myPdfStuff;
public MyPdfVM MyPdfStuff
{
get { return _myPdfStuff; }
set { _myPdfStuff = value; this.NotifyPropertyChanged(()=>this.MyPdfStuff);}
}
public MyViewmodel()
{
this.MyPdfStuff = new MyPdfVM();
}
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged<T>(Expression<Func<T>> property)
{
var propertyInfo = ((MemberExpression)property.Body).Member as PropertyInfo;
if (propertyInfo == null)
{
throw new ArgumentException("The lambda expression 'property' should point to a valid Property");
}
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyInfo.Name));
}
}
window.xaml
<Window x:Class="WpfBrowser.MainWindow"
xmlns:WpfBrowser="clr-namespace:WpfBrowser"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<DataTemplate DataType="{x:Type WpfBrowser:MyPdfVM}">
<WpfBrowser:BrowserControl />
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="64*" />
<RowDefinition Height="247*" />
</Grid.RowDefinitions>
<Button Content="Button" Height="23" HorizontalAlignment="Left" Margin="32,14,0,0" Name="button1" VerticalAlignment="Top" Width="75" Click="button1_Click" />
<ContentControl Grid.Row="1" Content="{Binding MyPdfStuff}"/>
</Grid>
</Window>
window.xaml.cs
public partial class MainWindow : Window
{
private MyViewmodel _data;
public MainWindow()
{
_data = new MyViewmodel();
InitializeComponent();
this.DataContext = _data;
}
private void button1_Click(object sender, RoutedEventArgs e)
{
this._data.MyPdfStuff = new MyPdfVM() { MyPdf = new Uri(#"your other pdf path for testing") };
}
}
when ever you change the MyPdfStuff Property the webbroswer update the pdf.
attached property
public static class WebBrowserUtility
{
public static readonly DependencyProperty BindableSourceProperty =
DependencyProperty.RegisterAttached("BindableSource", typeof(string), typeof(WebBrowserUtility), new UIPropertyMetadata(null, BindableSourcePropertyChanged));
public static string GetBindableSource(DependencyObject obj)
{
return (string)obj.GetValue(BindableSourceProperty);
}
public static void SetBindableSource(DependencyObject obj, string value)
{
obj.SetValue(BindableSourceProperty, value);
}
public static void BindableSourcePropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
WebBrowser browser = o as WebBrowser;
if (browser != null)
{
string uri = e.NewValue as string;
browser.Source = string.IsNullOrWhiteSpace(uri) ? null:new Uri(uri);
}
}
}
EDIT: added some code so you can see that if you chane the PDFViewmodel your browsercontrol show the new pdf.

wpf datagrid icollectionview sorting BUG?

maybe someone can help me? I have the following scenario:
A simple view:
<Window x:Class="DataGridSortBug.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">
<DockPanel>
<StackPanel DockPanel.Dock="Top">
<Button Click="Button_Click">Refresh</Button>
</StackPanel>
<DataGrid ItemsSource="{Binding View}" />
</DockPanel>
</Window>
The code behind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ViewModel();
}
public class TestItem
{
private int _sequence;
public int Sequence
{
get { return _sequence; }
}
public TestItem(int sequence)
{
_sequence = sequence;
}
}
public class ViewModel
{
ObservableCollection<TestItem> _collection;
private ICollectionView _view;
public ICollectionView View
{
get { return _view; }
}
public ViewModel()
{
_collection = new ObservableCollection<TestItem>();
_collection.Add(new TestItem(5));
_collection.Add(new TestItem(2));
_collection.Add(new TestItem(4));
_collection.Add(new TestItem(3));
_collection.Add(new TestItem(1));
_view = CollectionViewSource.GetDefaultView(_collection);
_view.SortDescriptions.Add(new SortDescription("Sequence", ListSortDirection.Ascending));
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
DataContext = new ViewModel();
}
}
After the program startup the datagrid contains (as expected):
1
2
3
4
5
After click on the button:
5
2
4
3
1
But I really can't understand why. Am I doing something wrong or is this a bug? And if this is a bug is there a workaround?
I just ran into this bug. (Or at least I presume it is a bug).
When debugging, you can see that the SortDescriptions collection gets cleared after assigning the ViewModel to the DataContext.
As a work around, I removed the SortDescriptions from the CTOR of the ViewModel and put them within a public method which I then call after assigning the ViewModel to the DataContext.
private void Button_Click(object sender, RoutedEventArgs e)
{
var model = new ViewModel();
DataContext = model; // SortDescriptions collection is cleared here.
model.AddSortDescriptions();
model.View.Refresh();
}
It is far from ideal, however this seems to be the only workaround I could find.
Try calling
_view.Refresh();
after adding the SortDescription.
Your TestItem is not implementing the IComparable interface so it is not sure of what to compare your objects by.
MSDN IComparable
Basically you need to add this to your class below.
public class TestItem : IComparable
{
private int _sequence;
public int Sequence
{
get { return _sequence; }
}
public TestItem(int sequence)
{
_sequence = sequence;
}
public int CompareTo(object obj)
{
if (obj == null)
return 1;
// put comparison logic here
}

Resources