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.
Related
I am using ComboBox in wpf as below and want to update ComboBox behind the seen if i update collection :-
<xmlns:dataProvider="clr-namespace:DataProvider"
<UserControl.Resources>
<dataProvider:BackOfficeDataProvider x:Key="DataProvider"/>
</UserControl.Resources>
<ComboBox x:Name="groupGroupNameCombo" HorizontalAlignment="Left" Margin="368,123,0,0" VerticalAlignment="Top" Width="226" Height="31" SelectionChanged="groupGroupNameCombo_SelectionChanged" DisplayMemberPath="GroupName" SelectedItem="{Binding ParentID, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" ItemsSource="{Binding GroupParentList, Mode=TwoWay, NotifyOnTargetUpdated=True, NotifyOnSourceUpdated=True, Source={StaticResource DataProvider}}" IsSynchronizedWithCurrentItem="True">
</ComboBox>
Class BackOfficeDataProvider {
public static ObservableCollection<Categories> groupParentList = null;
public virtual ObservableCollection<Categories> GroupParentList
{
get { return groupParentList ; }
set
{
groupParentList = value;
// Call OnPropertyChanged whenever the property is updated
OnPropertyChanged("GroupParentList");
}
}
public void loadComboListData();
{
GroupParentList = (ObservableCollection<Categories>) //fetching data from database using NHibernate directly getting list ;
}
}
my front end class which has refresh button :-
private void RefreshButton_Click(object sender, RoutedEventArgs e)
{
new BackOfficeDataProvider().loadComboListData();
}
when application load that time i can see the item in combobox but when i click on Refresh button that time it load updated data from database but not updating combobox untill i use below code
groupGroupNameCombo.ItemsSource = null;
groupGroupNameCombo.ItemSource = GroupParentList ;
Its a manually thing i have to do always to refresh combobox, how can i make it automatic like if i update collection then it should update combobox at the same time and i don't need to use above workaround.
I think this may have something to do with breaking the coupling between the combobox and the ObservableCollection when doing this:
GroupParentList = //fetching data from database;
Try this instead:
var dbCategories = // Get data from DB
GroupParentList.Clear();
foreach (var item in dbData)
GroupParentList.Add(item);
The point is to update the items in the collection, not the collection itself.
Also, try defining your collection like this, it should'nt have to be instantiated more than once (i.e no setter):
public static ObservableCollection<Categories> groupParentList = null;
public virtual ObservableCollection<Categories> GroupParentList
{
get
{
if (groupParentList == null)
groupParentList = new ObservableCollection<Categories>();
return groupParentList;
}
}
Hogler is right, your approach of assigning a new ObservableCollection object to the binding property will break how binding works. For ObservableCollection to work, you will need to modify the items in the collection itself, ObservableCollection is responsible of publishing list changes to the binding target. When you assign a new collection to the binding target, the list will not get refresh unless you published PropertyChanged event again to register this new binding source.
In your later comment you did state that you only instantiate ObservableCollection once only, which is not obvious from your posted code. It appears to me that the reason why it doesn't work is because you assign a new collection to the "GroupParentList" each time you run "loadComboListData".
Try this ..
once You are done getting data from your database in groupParentList , Add below Line, it will work as below :-
GroupParentList = new ObservableCollection<Categories>(groupParentList )
I have a WPF application (.NET 4.5) using MEF and MVVMLight. There is a shared MEF Export called "DeviceSupervisor" which holds an ObservableCollection of a custom class, DeviceConfiguration, which isnt much more than a few string properties.
In the main app, you can add and remove devices to/from this collection. One of these modules displays the list of devices in a ComboBox for choosing.
Explicit Conditions:
Have a number of items added to the collection via the main app
Select one of the items from the drop down in the module
Remove the item that happens to be selected from the collection via the main app
Code to sync user changes to DeviceSupervisor:
foreach (DeviceComplete set in Devices)
{
set.Configuration.CommunicationDetails = set.Device.Value.CommunicationDetails;
if (DeviceSupervisor.AvailableDevices.Any(d => d.ID == set.Configuration.ID))
{
DeviceSupervisor.AvailableDevices
.Single(d => d.ID == set.Configuration.ID)
.CommunicationDetails = set.Configuration.CommunicationDetails;
}
else
{
DeviceSupervisor.AvailableDevices.Add(set.Configuration);
}
}
var missing = new HashSet<DeviceConfiguration>(DeviceSupervisor.AvailableDevices
.Except(Devices.Select(d => d.Configuration)));
foreach (DeviceConfiguration toRemove in missing)
{
// --- TargetException thrown here ---
DeviceSupervisor.AvailableDevices.Remove(toRemove);
}
CollectionViewSource in XAML on the module:
<CollectionViewSource x:Key="AvailableDevicesCvs"
Source="{Binding Path=AvailableDevices, Mode=OneWay}" Filter="AvailableDevicesCVS_Filter">
</CollectionViewSource>
ComboBox:
<ComboBox HorizontalAlignment="Left" Margin="10,41,0,0" VerticalAlignment="Top" Width="120"
ItemsSource="{Binding Source={StaticResource AvailableDevicesCvs}}"
SelectedItem="{Binding Path=SelectedDevice}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=CommunicationDetails}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
Exception:
An unhandled exception of type 'System.Reflection.TargetException' occurred in mscorlib.dll
Additional information: Object does not match target type.
The stack trace is almost all WindowsBase and PresentationFramework references that are grayed out. Intellitrace only shows the exception being thrown on the line that I commented above.
My Findings / Other Notes
Note that this exception is not thrown if the ComboBox has another item selected than the removed. Throwing explicit one-way bindings everywhere had no affect. The exception is thrown before any breakpoints I can throw in CollectionChanged or Filter events.
After a lot of struggling, reorganizing, and trial and error, I found that if I simply remove the ItemTemplate from the combobox, then everything works as expected - no exceptions and the ComboBox adjusts the selection to compensate for the missing item.
I'm nervous that I'm caught in some tunnel vision trap and can't see the silly mistake I made - but more nervous that its something else.
I've since traced the issue to the CommunicationDetails property. The library with the source file for that class did not have a reference to MVVMLight, and as a quick shortcut to get a xyzChanged even (for other uses) I wrote the property like the following:
Old
private string mCommunicationDetails;
public string CommunicationDetails
{
get
{
return mCommunicationDetails;
}
set
{
bool changed = (mCommunicationDetails != value);
mCommunicationDetails = value;
if (changed)
{
CommunicationDetailsChanged.Raise(this, EventArgs.Empty);
}
}
}
public event EventHandler CommunicationDetailsChanged;
Replacing it with a vanilla {get;set;} property works. I've since replaced it with an MVVMLight property
New
private string mCommunicationDetails;
public string CommunicationDetails
{
get
{
return mCommunicationDetails;
}
set
{
if (Set(() => CommunicationDetails, ref mCommunicationDetails, value))
{
CommunicationDetailsChanged.Raise(this, EventArgs.Empty);
}
}
}
public event EventHandler CommunicationDetailsChanged;
And it works.
If anyone cares to take the time to find or explain the real source of this error I'll accept that as the answer, but I'm accepting this for now to close the issue.
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}"/>
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.
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.