I ran into a quite confusing problem when developing a multi-window wpf application.
There are two windows, MainWindow and SecondWindow. The code of both is pretty simple:
MainWindow:
<Button Content="Change Property to 5" Click="ChangeProperty" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" />
SecondWindow:
<Label Content="{Binding InstanceOfMyClass.value, NotifyOnSourceUpdated=True}"></Label>
The code behind the second Window is untouched, the code behind the first window is the following:
public partial class MainWindow : Window
{
SecondWindow w;
ViewModel vm;
public MainWindow()
{
InitializeComponent();
vm = new ViewModel() { InstanceOfMyClass = new MyClass() { value = 3 } };
w = new SecondWindow() { DataContext = vm };
w.Show();
}
private void ChangeProperty(object sender, RoutedEventArgs e)
{
vm.InstanceOfMyClass.value = 7;
}
}
And the view model class which implements INotifyPropertyChanged:
class ViewModel : INotifyPropertyChanged
{
private MyClass _instance;
public MyClass InstanceOfMyClass
{
get
{
return _instance;
}
set
{
_instance = value;
OnPropertyChanged("InstanceOfMyClass");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
class MyClass
{
public int value { get; set; }
}
I expected the text block to change its text to 5 when I click the button.
The number "3" is correctly loaded on startup. The window also refreshes when I create a new instance of MyClass and set it as InstanceOfMyClass in my ViewModel.
But when I hit the button - or, even stranger, when I temporarily store InstanceOfMyClass, set it to null and reassign it with the saved variable - nothing happens.
Any idea why?
Thanks in advance!
Implement INotifyPropertyChanged in MyClass and try again. In ChangeProperty you change the value property, that doesn't notify the view about the change.
Or you can also try rewriting your ChangeProperty to the following:
vm.InstanceOfMyClass = new MyClass() { value = 7 };
Both of these approaches should fix the problem as far as I can see.
Related
I have WPF application with Combobox and Button. After I enter value into textbox and press button, the value from textbox should appear in updated list of combobox. I am trying to achieve this with MVVM and binding to combobox. Here is part of code from ViewModel.
public class ViewModel:INotifyPropertyChanged
{
DomainLogic dl = new DomainLogic();
public event PropertyChangedEventHandler PropertyChanged;
private ObservableCollection<string> expenseCategories = new ObservableCollection<string>();
public ObservableCollection<string> ExpenseCategories
{
get
{
return expenseCategories;
}
set
{
expenseCategories = value;
OnPropertyChanged("ExpenseCategories");
}
}
public ViewModel()
{
expenseCategories = new ObservableCollection<string>(dl.GetExpenseCategories());
}
private void OnPropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
Also I am using EF to access DB and DomainLogic class has a method to list all Expense Categories.
Here is code-behind from window:
DomainLogic dl = new DomainLogic();
public Window1()
{
InitializeComponent();
DataContext = new ViewModel();
}
private void button1_Click(object sender, RoutedEventArgs e)
{
if (!string.IsNullOrWhiteSpace(textBox.Text))
{
dl.CreateNewExpenseCategory(textBox.Text);
}
else
{
MessageBox.Show("Enter category!");
}
}
Here is also XAML:
<ComboBox x:Name="ExpCategory" HorizontalAlignment="Left" Margin="72,50,0,0" VerticalAlignment="Top" Width="130" ItemsSource="{Binding ExpenseCategories, UpdateSourceTrigger=PropertyChanged}" />
When I add the new category the combobox isn't updated. I'm new to the whole MVVM pattern and I think I'm missing something here.
//EDIT
public void CreateNewExpenseCategory(string name)
{
using (var context = new ExpenseEntities())
{
ExpenseCategory category = new ExpenseCategory() { CategoryName = name};
context.ExpenseCategory.Add(category);
context.SaveChanges();
}
}
The problem is that CollectionChanged event doesn't fire.
You are adding a new element inside your DataContext, but you aren't updating your local view of data.
Once you update the DataContext you should refresh your ObservableCollection or use Local.
Here how you could use Local:
public ViewModel()
{
expenseCategories = dl.GetExpenseCategories().Local;
}
So you can directly do:
expenseCategories.Add(new ExpenseCategory() {textBox.Text});
dl.GetContext().SaveChanges();
Or you have to update the ObservableCollection:
dl.CreateNewExpenseCategory(textBox.Text);
// Update your ViewModel ObservableCollection.
However i think that you should use a Command and not an Event so you can update the ObservableCollection directly inside the ViewModel.
Example:
using Prism.Commands;
//Other usings
public class ViewModel : INotifyPropertyChanged
{
// Your class methods and properties
public DelegateCommand<string> AddNewExpenseCategory
{
get
{
return new DelegateCommand<string>(Execute_AddNewExpenseCategory);
}
}
public void Execute_AddNewExpenseCategory(string param)
{
expenseCategories.Add(new ExpenseCategory() { param });
dl.GetContext().SaveChanges();
}
im now trying for hours to get the databinding run.
But whatever im trying nothing works.
After a few thousend examples and retries (feels like a ffew thousand) i decided to make a new thread for my problem.
I have a window where you can select a woker.
On these window there are a UserControl which shows the details of the selected worker.
It would be nice to have all labels / textboxes / comboboxes filled automatically in case the selected worker changed.
For that the UserControl has a Property "ShownWorker" which contains the selected worker.
Worker Class:
public class Worker : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
private string id;
public string ID
{
get
{
return id;
}
set
{
id = value;
OnPropertyChanged("ID");
}
}
public Worker()
{
}
}
UserControl:
private Worker shownWorker;
public Worker ShownWorker
{
get
{
return shownWorker;
}
set
{
shownWorker = value;
}
}
public WorkerDetails()
{
InitializeComponent();
this.DataContext = shownWorker;
}
Label on the UserControl:
<Label Height="28" Margin="129,6,6,0" Name="labelWorkerID" VerticalAlignment="Top" Content="{Binding ID, Mode=OneWay, UpdateSourceTrigger=PropertyChanged, NotifyOnSourceUpdated=True}"></Label>
And im setting the ShownWorker like that:
private void dataGridAvaibleWorker_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (dataGridAvaibleWorker.SelectedItem is Worker)
{
var selectedWorker= (Worker)dataGridAvaibleWorker.SelectedItem;
WorkerDetails.ShownWorker = selectedWorker;
}
}
But nothing happens. Whats wrong?
I dont get it.
Aside from the property change notification, you have another issue:
These two properties:
ShownWorker
DataContext
Are pointing to the same reference when the form opens - e.g.
Worker someWorker = new Worker();
ShownWorker = someWorker;
DataContext = ShownWorker;
Changing ShownWorker here does not affect DataContext
ShownWorker = new Worker();
DataContext at this point still references the original worker - when you do this assignment DataContext = ShownWorker, DataContext references the worker you instantiated in the first of the three lines, it does not refer to the instance that ShownWorker is pointing to
To try and explain it a bit better:
// I'll stick some 'tags' on to shown how the instances will be referenced
Worker someWorker = new Worker(); // someWorker = "Instance1"
ShownWorker = someWorker; // ShownWorker points to "Instance1"
DataContext = ShownWorker; // DataContext points to "Instance1"
ShownWorker = new Worker(); // ShownWorker now points to "Instance2"
// DataContext still points to "Instance1"
You need to set DataContext not ShownWorker and raise a property changed event for DataContext
A better way to do this is to use an MVVM approach e.g.
public class WorkerViewModel : INotifyPropertyChanged
{
// Put standard INPC implementation here
public event PropertyChanged.... etc
private Worker shownWorker;
public Worker ShownWorker
{
get
{
return shownWorker;
}
set
{
if(value == shownWorker) return; // Don't do anything if the value didn't change
shownWorker = value;
OnPropertyChanged("ShownWorker");
}
}
public WorkerViewModel()
{
ShownWorker = // Get/create the worker etc
}
}
Now you have a VM you can set the DataContext to:
public WorkerViewModel WorkerViewModel { get; private set; }
public WorkerDetails()
{
InitializeComponent();
WorkerViewModel = new WorkerViewModel();
this.DataContext = WorkerViewModel;
}
And updates can be done to the VM instead:
WorkerViewModel.ShownWorker = someWorker;
Make sure you set your bindings in the XAML
<UserControl>
<SomeControl DataContext="{Binding ShownWorker}" />
</UserControl>
Instead of rolling your own MVVM stuff - I'd suggest looking at some popular MVVM frameworks as they can make working with WPF/Silverlight a breeze.
My personal fave is Caliburn Micro but there are plenty out there (MVVMLight, Prism, etc)
Why does my textbox fail to update when I try to update it from another class?
I've instantiated the MainWindow class in my Email class, but when I try to do
main.trending.Text += emailText;
Am I doing something wrong?
You should bind your data.
Model
public class YourData : INotifyPropertyChanged
{
private string _textBoxData;
public YourData()
{
}
public string TextBoxData
{
get { return _textBoxData; }
set
{
_textBoxData = value;
// Call OnPropertyChanged whenever the property is updated
OnPropertyChanged("TextBoxData");
}
}
// Create the OnPropertyChanged method to raise the event
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
XAML Binding
Set data context in Codebehind
this.DataContext = YourData;
Bind Property
<TextBox Text="{Binding Path=Name2}"/>
See #sa_ddam213 comment. Dont do something like MainWindow main = new MainWindow(); inside Email class. Instead, pass the MainWindow object you already have.
Following codes will work:
public class MainWindow
{
public void MethodWhereYouCreateEmailClass()
{
Email email = new Email;
email.Main = this;
}
}
public class Email
{
public MainWindow main;
public void MethodWhereYouSetTrendingText()
{
main.trending.Text += emailText;
}
}
But I dont say that is best practice. I just try to keep it close to your existing code i guess.
I'm new to WPF, so there's probably something basic I'm missing here. I have an application that looks like this:
<Window x:Class="MyApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Test Application" Height="647" Width="723" Background="#88B0FF">
<DockPanel Name="MainDock">
<Button DockPanel.Dock="Top" Margin="5,0,5,0" x:Name="PingButton" Click="PingButton_OnClick">Ping</Button>
<TextBox Text="{Binding Path=Output}" />
</DockPanel>
</Window>
The code-behind is like this:
public partial class MainWindow : Window
{
private Model _applicationModel = new Model();
public Model ApplicationModel {
get { return _applicationModel; }
set { _applicationModel = value; }
}
public MainWindow()
{
InitializeComponent();
this.DataContext = ApplicationModel;
ApplicationModel.Output = "Not clicked";
}
private void PingButton_OnClick(object sender, RoutedEventArgs e)
{
ApplicationModel.Output = "Clicked";
}
}
I have a small class called Model that implements INotifyPropertyChanged.
public class Model : INotifyPropertyChanged
{
public string Output { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
I run this application, and the text box displays the text "Not clicked". When I click the button, I would expect that the text would change. It does not. The "ApplicationModel" object is updated, and this is reflected in the DataContext; I have a breakpoint in the OnPropertyChanged() method, however, and it appears that it's never being called.
What am I doing wrong?
OnPropertyChanged() isn't being called because you're not calling it.
There is no special magic that wires up calls to OnPropertyChanged by itself, so you need to do it yourself.
Specifically, you should modify your Output property to call it when it changes (and it wouldn't hurt to do the same for your ApplicationModel property:
private string output;
public string Output
{
get { return output; }
set
{
if (output != value)
{
output = value;
OnPropertyChanged("Output");
}
}
}
If you're targeting .NET 4.5 you can utilize the CallerMemberName attribute to reduce boilerplate code; This article explains how to do so. Then you'll have something like this:
private string output;
public string Output
{
get { return output; }
set { SetProperty(ref output, value); }
}
If you're using .NET 4.0 or below, you can use expression trees, as described in this answer.
I have a Big problem there and trying to solve it for so long now...
So what I try to do:
- I add a Button to my Wrapgrid with the Codebehind file
- This button should change a variable which is the Source of an Image
Datenbank database = new Datenbank();
Binding bind = new Binding("ValueGet");
bind.Source = database;
bind.Mode = BindingMode.OneWay;
System.Windows.Controls.Button champbtn = new System.Windows.Controls.Button();
champbtn.Name = "btnAhri";
champbtn.Width = 60;
champbtn.Height = 60;
champbtn.Margin = new Thickness(4);
champbtn.SetBinding(Button.CommandProperty, bind);
champbtn.ToolTip = "Ahri";
champbtn.Content = "Press me";
WrapGrid.Children.Add(champbtn);
This works. I get my Button and its clickable.
Now as you can see I added some Command Binding to my other Class "Datenbank" which look like this:
public class Datenbank : INotifyPropertyChanged
{
private string _Source;
public string ImgSource
{
get { return _Source; }
set
{
_Source = value;
NotifyPropertyChanged("ImgSource");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string propertyname)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyname));
}
public DelegateCommand ValueGet{ get; set; }
public Datenbank()
{
ValueGet = new DelegateCommand(Ahri);
}
private void Ahri(object sender, EventArgs e)
{
System.Windows.Forms.MessageBox.Show("test");
ImgSource = "Ahri_Square_0.png";
}
}
Here is my DelegateCommand class:
public class DelegateCommand : ICommand
{
public delegate void SimpleEventHandler(object sender, EventArgs e);
private SimpleEventHandler _eventHandler;
public DelegateCommand(SimpleEventHandler eventHandler)
{
_eventHandler = eventHandler;
}
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
_eventHandler(this, new EventArgs());
}
}
As you can see the generated button should change the string of "ImgSource"
This variable is bound to an imagebox in my xaml code:
<Image Height="50" Name="image1" Stretch="Fill" Width="50" Source="{Binding ImgSource, Source={StaticResource database}}" />
This is ok aswell. So now my problem is, when I press the generated button my "test" messagebox appears, but the image does not change its source and I really dont know how to fix this.
When I manually add a button with the same command as the generated button above, it works fine!
<Button Command="{Binding ValueGet,Source={StaticResource database}}">Press ME</Button>
It instantly changes the image source and the picture appears, but not with the generatet one and this is important!
So I hope anyone can help me out with this, because I can't find the problem.
The image was not build as resource of application/assembly.
Let try.
Go to Solution explorer.
Right-click on the image
Select Properties
You will see "Build action"
Build action value should be "Resource".
I hope this help.