I have the following begginer project, and have a wierd effect. I want the button to add one element to the ComboBox every time it is clicked.
What happens is that when I start the app, click n times on the button and then open the ComboBox, it shows me n items as expected. But no matter how often I click the button atfer that, there will be no new items in the ComboBox.
MainWindow.xaml:
<Window x:Class="WPF_binding_combobox.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WPF_binding_combobox"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
<Button Content="Add item to combobox" Click="Button_Click" />
<ComboBox ItemsSource="{Binding Path=ComboBoxItems}" />
</StackPanel>
</Window>
MainWindow.xaml.cs (code-behind):
namespace WPF_binding_combobox {
using System.Windows;
public partial class MainWindow : Window {
public MainWindow() {
InitializeComponent();
this.DataContext = new La();
}
private void Button_Click(object sender, RoutedEventArgs e) {
La l = this.DataContext as La;
l.AddItem();
}
}
}
La.cs
namespace WPF_binding_combobox {
using System.Collections.Generic;
using System.ComponentModel;
class La : INotifyPropertyChanged {
private int counter;
private IList<string> cbItems;
public IList<string> ComboBoxItems {
get {
return this.cbItems;
}
set {
this.cbItems = value;
this.RaisePropertyChanged("ComboBoxItems");
}
}
public La() {
this.cbItems = new List<string>();
this.counter = 0;
}
public void AddItem() {
var temp = this.ComboBoxItems;
temp.Add("abc" + (++this.counter));
this.ComboBoxItems = temp;
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propertyName) {
var handler = this.PropertyChanged;
if (handler != null) {
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
I have tried the debugger, and it shows me that there are items added to the backing field and the RaisePropertyChanged is called. For some reason, the UI does not show me any thing that changes after the first click.
I have tried setting Mode=OneWay or Mode=TowWay, there was no change.
Why is the ComboBox not getting updated after I open it for the first time?
You are using IList which has no way of informing View that items has been added or removed after initial binding (like you said).
You should use ObservableCollection. As the name suggests, it works on observer pattern where your View will be observer.
public ObservableCollection<string> ComboBoxItems
{
get
{
return cbItems;
}
}
and in AddItem you only need to to this
public void AddItem()
{
ComboBoxItems.Add("abc" + (++this.counter));
}
Off Topic:
If you want to use MVVM pattern, don't write any code in Code-behind. Use Delegate Commands to bind to buttons. (Google will help).
Even assigning DataContext can be removed to XAML like this
<Window.DataContext>
<myassembly:LA />
</Window.DataContext>
Till then, have a local variable in View which would save you casting hackles everytime.
private La _dataContext;
public MainWindow()
{
InitializeComponent();
_dataContext = new La();
this.DataContext = _dataContext
}
private void Button_Click(object sender, RoutedEventArgs e)
{
_dataContext.AddItem();
}
Whether you are using an ObservableCollection<T> or an IList<T> doesn't really matter if you reset the source collection property each time you want to update the ComboBox.
You should either use a single instance of an ObservableCollection that you add all items to or set your source property to a new IList<string>.
If you modify your AddItem() method slightly it works as expected:
public void AddItem()
{
var temp = new List<string>(this.ComboBoxItems);
temp.Add("abc" + (++this.counter));
this.ComboBoxItems = temp;
}
Related
I have implemented something violating the MVVM pattern, and I wondered if there was a MVVM way of doing this.
I have a Window MainWindow, its DataContext is bound to a class called ViewModel which implements INotifyPropertyChanged.
I also implemented a Window ChildWindow which appears in a "Dialog" style when a button is clicked, using a RelayCommand. The DataContext of ChildWindow also binds to ViewModel. This Window is used to fill the details of a new list Item. I pass the View as a CommandParameter to the ViewModel, so that the ChildWindow can be centered in comparison to the MainWindow. This is not MVVM, and I would like to change this.
First, I implemented this in a non-MVVM way:
Here is my XAML for the button in MainWindow which opens the ChildWindow:
<Button Name="BtnInsert" Width="50" Margin="10" Command="{Binding OpenChildWindowCommand}" CommandParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}}">Add</Button>
Here is my simplified XAML for the ChildWindow:
<Window x:Class="HWE_Einteilen_Prototype.View.ListItemWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:HWE_Einteilen_Prototype.View"
mc:Ignorable="d"
Title="test" Height="400" Width="400">
<TextBox Width="50" Text="{Binding CurrentListItem.Id}" ></TextBox>
</Window>
And here is my (simplified) ViewModel Class:
public class ViewModel : INotifyPropertyChanged
{
private DataContext _ctx;
private ListItem _currentListItem;
private ObservableCollection<listItem> _listItems;
private ListItemWindow _listItemWindow;
private ICommand _openListItemWindowCommand;
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<ListItem> ListItems
{
get { return _listItems; }
set
{
_listItems = value;
OnPropertyChanged();
}
}
public ListItem CurrentListItem
{
get { return _currentListItem; }
set
{
_currentListItem = value;
OnPropertyChanged();
}
}
public ICommand OpenListItemWindowCommand
{
get { return _openListItemWindowCommand; }
set
{
_openListItemWindowCommand = value;
OnPropertyChanged();
}
}
public ViewModel()
{
OpenListItemWindowCommand = new RelayCommand(this.OpenNewListItemWindow, this.CanOpenListItemWindow);
}
private void OpenNewListItemWindow(object parameter)
{
CurrentListItem = new listItem(){Id = "testId"};
_listItemWindow = new StListItemWindow(){DataContext = this};
_listItemWindow.Owner = (MainWindow)parameter;
_listItemWindow.WindowStartupLocation = WindowStartupLocation.CenterOwner;
_listItemWindow.Closing += OnStListItemWindowClosing;
_listItemWindow.Show();
}
private bool CanOpenListItemWindow(object parameter)
{
return true;
}
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
What I have tried:
I have tried implementing a Behavior (from system.windows.interactivity) for the button opening the child window, so that it creates a new Window and does all the centering and owner stuff, and leaving only CurrentListItem = new listItem(){Id = "testId"}; in the command method. However, in this case binding to CurrentListItem in the ChildWindow throws an exception.
XAML Code for the MainWindow Button:
<Button Name="BtnInsert" Width="50" Margin="10" Command="{Binding OpenListItemWindowCommand}" Content="Add">
<i:Interaction.Behaviors>
<behaviors:BehButtonNewWindow></behaviors:BehButtonNewWindow>
</i:Interaction.Behaviors>
</Button>
Behavior Code:
class BehButtonNewWindow : Behavior<Button>
{
private StListItemWindow _ListItemWindow;
protected override void OnAttached()
{
AssociatedObject.Click += OnClickHandler;
}
protected override void OnDetaching()
{
AssociatedObject.Click -= OnClickHandler;
}
private void OnClickHandler(object sender, RoutedEventArgs routedEventArgs)
{
if (sender is Button button)
{
var win = Window.GetWindow(button);
if (win != null)
{
_ListItemWindow = new ListItemWindow
{
DataContext = win.DataContext,
Owner = win,
WindowStartupLocation = WindowStartupLocation.CenterOwner
};
_ListItemWindow.Show();
}
}
}
}
Code of Command Execute Method from ViewModel:
private void OpenNewStListItemWindow(object parameter)
{
CurrentListItem = new ListItem(){Id = "testId"};
}
What am I doing wrong?
Credit for this answer goes to Will (see comments)
On handling the window opening:
Opening a window is a UI concern. Simply handle the button click in the codebehind, construct a new window and stick the current VM in it. MVVM != no codebehind.
On handling vm code:
[...] If you mean that last little bit of code at the bottom, make it public and have the window call it before opening the new window. The UI is perfectly fine knowing about your view models. They're designed to display their state and bind to their properties.
Thanks for your help!
Hi :) I'm trying to figure out how the INotifyPropertyChanged work with a very basic application. I am simply having a button in my mainWindow and when you press it, it should fire of an event to update a textBox, which have been bound to a specific attribute. However, even though the events gets fired of, they are always null and therefore the textBox is not updated.
<Window x:Class="StockViewer.MainWindow"
<!--Just erased some basic xaml here-->
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:RandomGen/>
</Window.DataContext>
<Grid>
<Button Click="incr" Height="30" VerticalAlignment="Top" Background="DarkGoldenrod"></Button>
<TextBlock VerticalAlignment="Top" Margin="40" Text="{Binding price, UpdateSourceTrigger=PropertyChanged}" Background="Aqua"></TextBlock>
</Grid>
When the button is pressed, the price should change:
public partial class MainWindow : Window
{
private RandomGen gen;
public MainWindow()
{
gen = new RandomGen();
InitializeComponent();
}
private void incr(object sender, RoutedEventArgs e)
{
gen.price = 7;
}
}
class RandomGen : NotifiedImp
{
public RandomGen()
{
_i = 3;
}
private int _i;
public int price
{
get { return _i; }
set
{
_i = value;
OnPropertyChanged("price");
}
}
}
class NotifiedImp: INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this,new PropertyChangedEventArgs(propertyName));
}
}
}
It's just really strange, the handler is always null. Thank you :)
You have two instances of RandomGen, one initialized in your XAML:
<Window.DataContext>
<local:RandomGen/>
</Window.DataContext>
And another initialized in your MainWindow constructor:
gen = new RandomGen();
This means when you update gen.price = 7; you aren't updating the instance which is your DataContext.
One solution would be remove your <Window.DataContext> setting in XAML and set DataContext in your MainWindow constructor, like so:
public MainWindow()
{
gen = new RandomGen();
InitializeComponent();
DataContext = gen;
}
The most MVVM like solution would be to use a ICommand on your RandomGen object to update price rather than using an event handler, then use this command in your XAML, like:
<Button Command="{Binding IncrementPriceCommand}"></Button>
Then it is up to you how you initialize DataContext, you wouldn't need to keep the RandomGen backing field either way.
I'm struggling to find a solution to my binding issue.
I have a User Control, which has a button for calling a separate window, in which the user can select an object. Upon selecting this object the window closes and an object in the user control has it's properties updated according to the selection.
The properties of this object are bound to controls in the user control, but when I update the properties in the object, the values in the controls are not updated (I hope that makes sense).
here is a slimmed down code behind:
public partial class DrawingInsertControl : UserControl
{
private MailAttachment Attachment { get; set; }
public DrawingInsertControl(MailAttachment pAttachment)
{
Attachment = pAttachment;
InitializeComponent();
this.DataContext = Attachment;
}
private void btnViewRegister_Click(object sender, RoutedEventArgs e)
{
DocumentRegisterWindow win = new DocumentRegisterWindow();
win.ShowDialog();
if (win.SelectedDrawing != null)
{
Attachment.DwgNo = win.SelectedDrawing.DwgNo;
Attachment.DwgTitle = win.SelectedDrawing.Title;
}
}
}
and the xaml:
<UserControl x:Class="DrawingInsertControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="310" d:DesignWidth="800" >
<Border BorderBrush="Black" BorderThickness="2" Margin="10">
<Grid>
...
<TextBox Grid.Column="1" Name="txtDocNo" Text="{Binding DwgNo}" />
and finally the attached object which is in a separate module:
Public Class MailAttachment
Public Property DwgNo As String
End Class
I've omitted namespaces and other stuff I didn't see as relevant.
Thanks in advance for any help.
Your MailAttachment class should implement INotifyPropertyChanged Interface:
public class MailAttachment: INotifyPropertyChanged
{
private string dwgNo;
public string DwgNo{
get { return dwgNo; }
set
{
dwgNo=value;
// Call NotifyPropertyChanged when the property is updated
NotifyPropertyChanged("DwgNo");
}
}
// Declare the PropertyChanged event
public event PropertyChangedEventHandler PropertyChanged;
// NotifyPropertyChanged will raise the PropertyChanged event passing the
// source property that is being updated.
public void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
This will force your control to observe PropertyChanged event. So your control can be notified about changes.
The code I provided is on C#, but, I hope you can translate it to VB.Net.
I am setting the DataContext for my View in the View's Constructor to an instance of my ViewModel, just standard stuff. Shortly thereafter, an UPDATE_RECENT_DOCUMENTS_LIST Event fires from the Event Aggregator which my ViewModel catches correctly. A property is changed and the onPropertyChanged method is called, but it fails as the PropertyChanged event is null.
The very next thing I do is an action to the UI which raises a CREATE_PROJECT Event and the same ViewModel is receiving events, except now, the PropertyChanged event is no longer null and everything works as expected.
Is there a specific amount of time that has to pass after setting the DataContext before it registers to the PropertyChanged Event? Is there an event I can wait for that ensures the PropertyChanged event is not null?
Also, I did not run into this problem using standard .NET events, just after integrating Prism and using the very convenient EventAggregator.
I am showing my code behind of the View and the ViewModel, omitting the View XAML for brevity.
ToolBarView.xaml.cs:
namespace ToolBarModule
{
public partial class ToolBarView : UserControl
{
public ToolBarView(ToolBarViewModel toolBarViewModel)
{
InitializeComponent();
this.DataContext = toolBarViewModel;
}
}
}
ToolBarViewModel.cs
namespace ToolBarModule
{
public class ToolBarViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private ToolBarCommands baseCommands;
private IEventAggregator eventAggregator;
private KickStartEvent kickStartEvent;
private SubscriptionToken subscriptionToken;
private ObservableCollection<IDocumentReference> recentDocuments = new ObservableCollection<IDocumentReference>();
private ActionCommand newTest;
private ActionCommand openTest;
private ActionCommand saveTest;
private ActionCommand exitApplication;
public ToolBarViewModel(){}
public ToolBarViewModel(IEventAggregator eventAggregator)
{
this.eventAggregator = eventAggregator;
baseCommands = new ToolBarCommands(eventAggregator);
kickStartEvent = eventAggregator.GetEvent<KickStartEvent>();
subscriptionToken = kickStartEvent.Subscribe(kickStartEventHandler, ThreadOption.UIThread, true, toolBarEventHandlerFilter);
}
public ICommand NewTest
{
get
{
if (newTest == null)
{
newTest = new ActionCommand(baseCommands.NewTestAction);
}
return newTest;
}
}
public ICommand OpenTest
{
get
{
if (openTest == null)
{
openTest = new ActionCommand(baseCommands.OpenTestAction);
}
return openTest;
}
}
public ICommand SaveTest
{
get
{
if (saveTest == null)
{
saveTest = new ActionCommand(baseCommands.SaveTestAction);
}
return saveTest;
}
}
public ICommand ExitApplication
{
get
{
if (exitApplication == null)
{
exitApplication = new ActionCommand(baseCommands.ExitApplicationAction);
}
return exitApplication;
}
}
public ObservableCollection<IDocumentReference> RecentDocuments
{
get
{
return recentDocuments;
}
set
{
recentDocuments = value;
onPropertyChanged("RecentDocuments");
}
}
private void onPropertyChanged(string propertyChanged)
{
if (PropertyChanged != null)
{
PropertyChanged(this,new PropertyChangedEventArgs(propertyChanged));
}
}
private void kickStartEventHandler(KickStartEventsArgs e)
{
switch (e.EventType)
{
case KickStartEventsArgs.KickStartEventType.CREATE_PROJECT:
onPropertyChanged("RecentDocuments");
break;
case KickStartEventsArgs.KickStartEventType.UPDATE_RECENT_DOCUMENTS_LIST:
RecentDocuments.Clear();
foreach (IDocumentReference recentDocs in e.KickStartTestList)
{
RecentDocuments.Add(recentDocs);
}
onPropertyChanged("RecentDocuments");
break;
}
}
}
}
You can also try to set the DataContext of a Grid or an Element below the UserControl. For me it worked.
Example (Doesn't work if you use DependencyProperty):
Code Behind:
public MyUserControl()
{
InitializeComponent();
this.DataContext = new { LabelText = "Hello World!" };
}
XAML
<UserControl x:Class="CoolProject.ViewModel.MyUserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Label x:Name="myLabel" Content="{Binding LabelText}"/>
Example 2 (My working code):
Code Behind:
public MyUserControl()
{
InitializeComponent();
this.myGrid.DataContext = new { LabelText = "Hello World!" };
}
XAML
<UserControl x:Class="CoolProject.ViewModel.MyUserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid x:Name="myGrid">
<Label x:Name="myLabel" Content="{Binding LabelText}"/>
</Grid>
You have to name your UserControl in XAML and use it in binding. Something like following code:
<UserControl x:Name="uc" >
.
.
.
<TextBox Text="{Binding UserName, Mode=TwoWay, ElementName=uc}"/>
Where uc is a name of your UserControl, and Also try to set DataContext when UserControl loaded.
Hope this help.
I cannot get any display from my observable collection in a custom object bound to a ListBox. This works fine when I have a string collection in my view model, but no names display when I try to access the property through a custom object. I am not receiving any errors in the output window.
Here is my code:
Custom Object
public class TestObject
{
public ObservableCollection<string> List { get; set; }
public static TestObject GetList()
{
string[] list = new string[] { "Bob", "Bill" };
return new TestObject
{
List = new ObservableCollection<string>(list)
};
}
}
Xaml
<Window x:Class="TestWPF.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>
<ListBox Height="100" HorizontalAlignment="Left" Margin="120,61,0,0" Name="listBox1" VerticalAlignment="Top" Width="120" ItemsSource="{Binding Path=TObj.List}" />
</Grid>
Xaml.cs
public partial class MainWindow : Window
{
private ModelMainWindow model;
public MainWindow()
{
InitializeComponent();
model = new ModelMainWindow();
this.DataContext = model;
this.Loaded += new RoutedEventHandler(MainWindow_Loaded);
}
public void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
this.model.Refresh();
}
}
ViewModel
public class ModelMainWindow : INotifyPropertyChanged
{
private TestObject tObj;
public event PropertyChangedEventHandler PropertyChanged;
public TestObject TObj
{
get
{
return this.tObj;
}
set
{
this.tObj = value;
this.Notify("Names");
}
}
public void Notify(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
public void Refresh()
{
this.TObj = TestObject.GetList();
}
}
Can't bind to private properties. Also the change notification targets the wrong property, change "Names" to "TObj". (Also i would recommend making the List property get-only (backed by a readonly field), or implementing INoptifyPropertyChanged so the changes cannot get lost)
Your List is private. Make it a public property otherwise WPF can't see it.