I try to understand data bindings in WPF and I could already bring some tests to work. But I'm stuck tight now. :(
For better organization I want to split my code into more classes and bind elements of the same Window to different classes (actually to properties of different classes).
I think I have to set the DataContext of my Window to "this" (The window itself) and use the Binding Path to specify the property to use.
DataContext = this;
-
<Label Content="{Binding Path=_printSettings.CopyCount}"/>
So f.e. I want to bind to CopyCount wich is a property returning a string. That property belongs to the instance in the private field _printSettings of the current window. And _printSetting implements the INotifyPropertyChanged and notifies in a twoway principle.
But the label is empty during design and runtime...
I also noticed that no default values are set in the designer in my previous tests. Does anyone know an implementation? If possible without the use of a fallback value.
-MainWindow.xaml.cs
<Label Content="{Binding ElementName=MainWindow,Path=PrintSettings.CopyCount, FallbackValue=[0]}">
-MainWindow.cs
private PrintSettings _printSettings = new PrintSettings();
public PrintSettings PrintSettings {
get {
return _printSettings;
}
}
public MainWindow()
{
DataContext = this;
}
PrintSettings.cs
private int _copyCount = 1;
//Copy count
public string CopyCount
{
get {
return "" + _copyCount;
}
}
-
EDIT:
added more code again
Binding works against/through public properties. _printSettings is not a property. The Visual Studio "Output" window can show any binding errors you have.
_printSettings and CopyCount should be public.
If it doesnt help, then in xaml Set window Name and binding will look like this
<Label Content="{Binding ElementName=YourWindowName,Path=_printSettings.CopyCount}"/>
or
<Label Content="{Binding ElementName=YourWindowName,Path=DataContext._printSettings.CopyCount}"/>
Related
I am building a WPF app and figured I would use AutoMapper to make copies of objects in my viewmodels. The problem I am having is that appears that AutoMapper is attempting to use a bound control as a source value and I don't understand why. I am new to AutoMapper so I figure I am missing some detail.
Details
UI has a list box and three buttons (Add, Edit, Delete). If nothing is selected in the list box, then only the Add button is active.
If an item is selected in the list box then all buttons are active.
If the Add button is clicked, a new empty object is created with properties bound to text boxes in the UI.
If the Edit button is clicked, a copy of the item selected in the list box is made and the copy's properties are bound to the text boxes in the UI.
All of this works. The problem occurs if I try to use AutoMapper to make the copy.
Here is the code in question. I have included what works (doing the copy manually, property by property) and the code that fails when using AutoMapper.
There are three properties in the viewmodel involved:
// Bound to the list box's ItemsSource property
public ObservableCollection<CarType> CarTypes
{
get { return _carTypes; }
set { SetProperty(ref _carTypes, value); }
}
// Bound to the list box's SelectedIndex property
public int SelectedIndex
{
get { return _selectedIndex; }
set
{
SetProperty(ref _selectedIndex, value);
}
}
// Properties of CarTypeDto are bound to text boxes in the Edit UI
public CarType CarTypeDto
{
get
{
// When the value is retrieved, a copy of the selected item
// is made and the copy is returned.
if (SelectedIndex != -1)
{
// What works==========
// CarType newDto = new CarType();
// CarType src = CarTypes[SelectedIndex];
// newDto.Id = src.Id;
// newDto.Name = src.Name;
// newDto.Description = src.Description;
// _carTypeDto = newDto;
// What does NOT work============
CarType src = CarTypes[SelectedIndex];
CarType newDto = _mapper.Map<CarType>(src); <=== fails here
_carTypeDto = newDto;
// Incorrect Solution #1 ======================
// Changed the above two lines to this solves the problem:
_mapper.Map<CarType, CarType>(src,_carTypeDto);
}
return _carTypeDto;
}
set
{
SetProperty(ref _carTypeDto, value);
}
}
When I try to use AutoMapper it gets to the line that fails and the debugger shows this error:
System.Windows.Data Error: 17 : Cannot get 'CarTypeDto' value (type 'CarType') from '' (type 'CarTypesViewModel'). BindingExpression:Path=CarTypeDto.Description; DataItem='CarTypesViewModel' (HashCode=66939890); target element is 'TextBox' (Name='DescriptionTextBox'); target property is 'Text' (type 'String')
What is confusing me is that I get a reference to the selected item (src) and attempt to map that to a new instance of CarType (newDto). Neither of these items (src or newDto) is part of the binding to the DescriptionTextBox in the editing UI. Why is the binding becoming an issue when I use AutoMaper?
The commented code (manually copying properties) works fine.
In case it helps, here are the bindings in question:
<ListBox x:Name="ContentView"
DockPanel.Dock="Top"
ItemsSource="{Binding CarTypes}"
Background="{StaticResource ControlBackgroundBrush}"
Foreground="{StaticResource ControlForegroundBrush}"
BorderBrush="{StaticResource ControlForegroundBrush}"
SelectedIndex ="{Binding SelectedIndex}" >
<TextBox Grid.Row="1"
Grid.Column="0"
Name="NameTextBox"
Margin="5"
Width="100"
MaxLength="10"
Text="{Binding CarTypeDto.Name, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}" />
<TextBox Grid.Row="1"
Grid.Column="1"
Grid.ColumnSpan="2"
Name="DescriptionTextBox"
Margin="5"
Width="300"
MaxLength="30"
Text="{Binding CarTypeDto.Description,
Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
In the bootstrap portion of the app I initialize the mapper like so...
private void InitializeAutomapper()
{
var mapperConfig = new MapperConfiguration(cfg =>
{
cfg.CreateMap<CarType, CarType>();
});
_mapper = new Mapper(mapperConfig);
}
...and then inject _mapper into the viewmodels as needed. I have tested the mapper and it will correctly map objects. The issue shows up when I attempt to do the mapping in the CarTypeDto property shown above.
==============================
EDIT - Note about the solution
I wanted to add a note about the solution for folks who may read this later - especially folks newer to AutoMapper and property binding (like myself).
The answer below made me realize that my approach to populating the CarTypeDto property was not correct. In the final solution I revamped the property to this:
public CarType CarTypeDto
{
get { return _carTypeDto; }
set { SetProperty(ref _carTypeDto, value); }
}
Then, I update the value to the CarTypeDto property elsewhere when needed. For example when the Edit button is clicked, I then retrieve the selected value and map it to CarTypeDto. The code shown at the top of this post is not the appropriate way to populate the value for a bound property.
While all of this had nothing to do with AutoMapper, when I tried to use AutoMapper it brought the larger problem to light.
The error mentioned by you is a binding error not the Automapper exception message.
System.Windows.Data Error: 17 : Cannot get 'CarTypeDto' value (type 'CarType') from '' (type 'CarTypesViewModel'). BindingExpression:Path=CarTypeDto.Description; DataItem='CarTypesViewModel' (HashCode=66939890); target element is 'TextBox' (Name='DescriptionTextBox'); target property is 'Text' (type 'String')
Now about mapping is not working ---- Before using Automapper, the map should be created so that AutoMapper can understand how to map one object/type to another. In your scenario, since you need to type from same type then you should create a map like
_mapper.CreateMap<MyType, MyType>();
The CreateMap should be called in Automapper profile initialization.
Update on your binding error:
The binding error is appearing as you are trying to create a new Object of CarType everytime in Get property so CarTypeDto property is not pointing to the object which was bound initially at XAML. Instead of creating a new object by mapper you should work on same object and update the property.
I spent some time now trying to work this out but I am still stuck on it. I have a WPF application with a MVVM pattern. In my ViewModel I have three cases where:
X needs Y and Y is available
X needs Y and Y is not available
X doesn't need Y
I am trying to set an image icon on my view based on these conditions (Like a Check Mark, exclamation Mark... whatever).
In ViewModel:
I created my properties. On any GUI change, I set the variables based on the above cases similar to the following:
void MyBigFunctionToSetAllProperties()
{
// other cases
// ..
if (NeedsY && YExists)
{
// properties
StatusIconPath = "#\"/Resources/SessionView/X-With-Green-Check-White.png\"";
ResultIconPath = "#\"/Resources/SessionView/Y-White.png\"";
}
}
In View.Cs: I did literally nothing.
In View.xaml: I bind like this:
<StackPanel>
<Image Source="{Binding StatusIconPath} />
</StackPanel>
I still can't see why it is not working. What is that thing that I am missing? Please and thanks.
It did not work to bind the properties directly with the Xaml as recommended. I tried it this way:
VM: sample property:
public BitmapImage VerificationStatusIcon{ get { return new BitmapImage(new Uri(#VerificationStatusIconPath, UriKind.Relative));}}
View Xaml:
<Image Name="verificationStatusImage" Source="{Binding VerificationStatusIcon}" Margin="5,0" Width="40" Height="40"/>
You have a whole bunch of unnecessary characters in your icon paths:
StatusIconPath = "#\"/Resources/SessionView/X-With-Green-Check-White.png\"";
ResultIconPath = "#\"/Resources/SessionView/Y-White.png\"";
Change them to this:
StatusIconPath = "Resources/SessionView/X-With-Green-Check-White.png";
ResultIconPath = "Resources/SessionView/Y-White.png";
. But no images originally to view and no changes..
Verify that the path to the image is correct. Maybe hard code an image to test the control against it.
One other scenario is that the resources are not being copied over for run-time acquisition. Make sure they are actually available during runtime.
can't see why it is not working
Is the main view's DataContext set to the live VM's instance?
What is that thing that I am missing?
If you are sure that the view's datacontext contains the live VM, then make sure that the property StatusIconPath on the VM reports a property change event.
That is so that the XAML control which is bound to it knows that it changed and correspondingly one needs to make sure that the ViewModel which holds StatusIconPath adheres to INotifyPropertyChanged which will facilitate such an operation in general:
private string _StatusIconPath;
public string StatusIconPath
{
get { return _StatusIconPath; }
set
{
_StatusIconPath = value;
PropertyChanged("StatusIconPath");
}
}
I provide more robust example on my blog entitled:
Xaml: ViewModel Main Page Instantiation and Loading Strategy for Easier Binding
It turned out that I have an extra unneeded characters in my ImagePaths as Kyle stated. And then, I needed to set my Image.Source from within my View.cs. At least, this is how it worked for me:
ViewModel Something like this:
if (Whatever)
{
StatusIconPath = "/Resources/SessionView/X-With-Green-Check-White.png";
ResultIconPath = "/Resources/SessionView/Y-White.png";
}
Then in View.cs and on SelectedItemChanged:
private void Grid_SelectedItemChanged(object sender, DevExpress.Xpf.Grid.SelectedItemChangedEventArgs e)
{
string tempStatus = ((SessionViewModel) DataContext).StatusIconPath;
string tempResult = ((SessionViewModel) DataContext).ResultIconPath;
StatusImage.Source = new BitmapImage(new Uri(#tempStatus, UriKind.Relative));
ResultImage.Source = new BitmapImage(new Uri(#tempResult, UriKind.Relative));
}
and in Xaml: just a fallback value(any original/default image we want). Ex:
<Image Name="ResultImage" Source="/EZ3D;component/Resources/SessionView/Retake-White.png"/>
In my program's main window I have a TreeView and a ContentPresenter. The display of the ContentPresenter is determined by what node is selected in the TreeView.
The name of one of my nodes is allowed to be changed by the user via contentMenu. All the user has to do is right click the node and select the new name out of the choices. The ContentPresenter is supposed to have a null display until the user chooses a name for the node.
The problem occurs when a new name is selected from the contentMenu. The ContentPresenter's display changes, like it should, but only after the user selects a different node (changing the display), and then re-selects the original node.
How do I make it so that the display on the ContentPresenter changes right when the TreeView node's name is changed?
TreeViewViewModel:
public class TreeViewViewModel : PropertyChangedBase
{
public TreeViewViewModel()
{
Node = new Node() { NodeName = "Blank", NodeDataModel = new NodeModel(),
Commands = { new Command(nodeType_name1), new Command(nodeType_name2) } };
}
//These functions call to the NodeName property in the TreeView's Data Model
private void nodeType_name1()
{
Node.NodeName = "Name1";
}
private void nodeType_name2()
{
Node.NodeName = "Name2";
}
}
XAML for MainWindow:
<!-- Tree view items & Functions -->
<TreeView Name="Tree_One" ItemsSource="{Binding DataTree.Data}" ... >
<TreeView.Resources>
<SolidColorBrush Color="LightSkyBlue" x:Key="{x:Static SystemColors.HighlightBrushKey}" />
</TreeView.Resources>
</TreeView>
<!--- Left Widget -->
<ContentPresenter Content="{Binding LeftWidget}" />
MainWindowViewModel:
public class MainWindowViewModel : PropertyChangedBase
{
private TreeViewViewModel _dataTree;
public MainWindowViewModel()
{
_dataTree = new TreeViewViewModel();
}
public TreeViewViewModel DataTree { ... }
//This function is in charge of changing the display of the ContentPresenter
// I think that my problem can probably be solved by doing something here
public void ChangeViews()
{
if (_dataTree.SelectedItem is Node)
{
var _node = _dataTree.SelectedItem as Node;
var nodeViewModel = new NodeViewModel(_node.NodeDataModel);
if (_node.NodeName== "Unknown")
LeftWidget = null; //This is the Content Presenter **
if (_node.NodeName == "Name1")
{
LeftWidget = nodeViewModel;
}
if (_node.NodeName == "Name2") {...}
}
}
}
Duh, thats a alot of code and its pretty difficult to understand what you up to since you seem to have controls in your ViewModel.
Or at least it looks to me that you have them in ViewModel. That is not very MVVM-alike my friend. :)
"The problem occurs when a new name is selected from the contentMenu. The ContentPresenter's display changes, like it should, but only after the user selects a different node (changing the display), and then re-selects the original node."
The property changed is not being fired because the new selected value is equal to the old one.
Pretty obvious, right?... no property was actually changed
But why do you want the ContentPresenter to update itself with the value that it already has?
You said when you select a node the ContentPresenter displays it properly and when you re-select the same the ContentPresenter is not doing anything.
Its not doing anything because it think it doesnt need to. Which is true.
So the question is why would you make ContentPresenter force to refresh on each value no matter if old value is the same as new one?
Though if you want to hack/trick a little bit, you can always set ContentPresenter's Content to null before you assign another value. :)
However, post us more code and we will be able to provide you a better solution to your issue.
I was able to fix this issue by calling ChangeViews(); in my MainWindowViewModel from my TreeViewViewModel. I did this by using a delegate property in the TVVM, and adding it to my MWVM. By doing this, the display is updated whenever ChangeViews(); is called.
This is the answer that I used.
Next code works as expected:
AskWindow.xaml:
<Window
x:Class='AskWPF.AskWindow'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
>
<DataGrid ItemsSource="{Binding SimpleItems}" />
</Window>
AskWindow.xaml.cs:
namespace AskWPF {
public class SimpleRow {
private string firstColumn;
private string secondColumn;
public SimpleRow(string first, string second) {
firstColumn = first;
secondColumn = second;
}
public string FirstColumn {
get { return firstColumn; }
set { firstColumn = value; }
}
public string SecondColumn {
get { return secondColumn; }
set { secondColumn = value; }
}
}
public partial class AskWindow : Window {
private ObservableCollection<SimpleRow> simpleItems;
public AskWindow() {
InitializeComponent();
DataContext = this;
simpleItems = new ObservableCollection<SimpleRow>();
simpleItems.Add(new SimpleRow("row 0, column 0", "row 0, column 1"));
simpleItems.Add(new SimpleRow("row 1, column 0", "row 1, column 1"));
}
public ObservableCollection<SimpleRow> SimpleItems {
get { return simpleItems; }
}
}
}
But if set DataContext='{Binding RelativeSource={RelativeSource Self}}' in Window tag and comment line DataContext=this we get an empty window. Why?
AskWindow.xaml:
<Window .... DataContext='{Binding RelativeSource={RelativeSource Self}}'>
<DataGrid ItemsSource="{Binding SimpleItems}" />
</Window>
AskWindow.xaml.cs:
...
public AskWindow() {
InitializeComponent();
// DataContext = this;
simpleItems = new ObservableCollection<SimpleRow>();
simpleItems.Add(new SimpleRow("row 0, column 0", "row 0, column 1"));
simpleItems.Add(new SimpleRow("row 1, column 0", "row 1, column 1"));
}
...
Here is my guess. In both cases at one point your Collection is null. To be precise right after InitializeComponent. At this point the initial databinding got the data, but no datacontext. Now by setting the DataContext your property gets raised and every binding related to it, gets invalidated and refreshed. Here is my guessing part, the reason it works is that the binding to the ItemsSource is deferred therefore it works to just set the collection in the next line.
So in short: Setting the Datacontext will retrigger the binding. But in your RelativeSource example your binding worked from the beginning but the collection was null and you never told wpf to refetch the binding.
If you directly initialize your collection it should work fine.
I suspect it has to do with how and when certain kinds of bindings are evaluated. In the latter case i think that the binding may retrieve the value of the collection property while it still is null, then you change the property (by setting the field) without firing any change notification for the affected property.
Would recommend to move the InitializeComponent call to the end of the constructor or to at least set the field beforehand.
Usually i use a readonly field and just initialize it right away:
private readonly ObservableCollection<Data> collection =
new ObservableCollection<Data>();
public ObservableCollection<Data> Collection { get { return collection ; } }
Actually the binding is correct and it works also. In order the screen to be updated the binding has to receive notifications that something changed. A binding first evaluates and then listens for notifications. In your second version the binding first evaluates when InitializeComponent is run, but there are no values at that moment so you see nothing. After that the values are created but the binding does not reevaluate because no notifications are sent.
So yes one solutuion would be to initialize the collection prior to InitializeComponent.
...
private ObservableCollection<SimpleRow> simpleItems = new ObservableCollection<SimpleRow>();
...
Another solution would be stupid and an overkill to notify the binding that something was changed.
Just a note, probably this is for learning purposes because the UI should not be mixed up with the model.
I know I am missing something here and I could use a pointer. Within a project I have an expander control when this control is clicked it makes a RIA call to a POCO within my project to retreive a second set of data. I am using the SimpleMVVM toolkit here so please let me know if I need to expand on any additional areas.
Within the xaml the expander is laid out as
<toolkit:Expander Header="Name" Style="{StaticResource DetailExpanderSytle}" >
<i:Interaction.Triggers>
<i:EventTrigger EventName="Expanded">
<ei:CallMethodAction
TargetObject="{Binding Source={StaticResource vm}}"
MethodName="showWarrantNameDetail"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<StackPanel Orientation="Vertical">
<sdk:DataGrid AutoGenerateColumns="true" ItemsSource="{Binding NameResult}" AlternatingRowBackground="Gainsboro" HorizontalAlignment="Stretch" MaxHeight="200">
</sdk:DataGrid>
<local:NameContainer DataContext="{Binding}" />
</StackPanel>
</toolkit:Expander>
I am using the expression Dll coupled with Simple MVVM to get at the methods in the view model vs commands.
Within the view model I have the following code
public void showWarrantNameDetail()
{
//set flags
IsBusy = true;
CanDo = false;
EntityQuery<WarrantNameDataView> query = App.cdContext.GetWarrantNameDataViewsQuery().Where(a => a.PrimaryObjectId == Convert.ToInt32(RecID));
Action<LoadOperation<WarrantNameDataView>> completeProcessing = delegate(LoadOperation<WarrantNameDataView> loadOp)
{
if (!loadOp.HasError)
{
processWarrantNames(loadOp.Entities);
}
else
{
Exception error = loadOp.Error;
}
};
LoadOperation<WarrantNameDataView> loadOperation = App.cdContext.Load(query, completeProcessing, false);
}
private void processWarrantNames(IEnumerable<WarrantNameDataView> entities)
{
ObservableCollection<WarrantNameDataView> NameResult = new ObservableCollection<WarrantNameDataView>(entities);
//we're done
IsBusy = false;
CanDo = true;
}
When I set a break on the processWarrantName I can see the NameResult is set to X number of returns. However within the view the datagrid does not get populated with anything?
Can anyone help me understand what I need to do with the bindings to get the gridview to populate? Other areas of the form which are bound to other collections show data so I know I have the data context of the view set correctly. I've tried both Data context as well as Items Source and no return?
When I set a break on the code the collection is returned as follows so I can see that data is being returned. Any suggestions on what I am missing I would greatly appreciate it.
With regards to the page datacontext I am setting it in the code behind as follows:
var WarrantDetailViewModel = ((ViewModelLocator)App.Current.Resources["Locator"]).WarrantDetailViewModel;
this.DataContext = WarrantDetailViewModel;
this.Resources.Add("vm", WarrantDetailViewModel);
Thanks in advance for any suggestions.
Make ObservableCollection<WarrantNameDataView> NameResult a public property of your ViewModel class. Your view will not be able to bind to something that has a private method scope (or public method scope, or private member scope).
//declaration
public ObservableCollection<WarrantNameDataView> NameResult { get; set }
//in the ViewModel constructor do this
NameResult = new ObservableCollection<WarrantNameDataView>();
//then replace the original line in your method with:
//EDIT: ObservableCollection has no AddRange. Either loop through
//entities and add them to the collection or see OP's answer.
//NameResult.AddRange(entities);
If processWarrantNames gets called more than once, you might need to call NameResult.Clear() before calling AddRange() adding to the collection.
Phil was correct in setting the property to public. One note I'll add is there is no AddRange property in SL or ObservableCollection class that I could find. I was able to assign the entities to the OC using the following code
private ObservableCollection<WarrantNameDataView> warrantNameResult;
public ObservableCollection<WarrantNameDataView> WarrantNameResult
{
get { return warrantNameResult; }
set
{
warrantNameResult = value;
NotifyPropertyChanged(vm => vm.WarrantNameResult);
}
}
and then within the return method
WarrantNameResult = new ObservableCollection<WarrantNameDataView>(entities);
This worked and passed to the UI the collection of data.