An item with the same key has already been added

Dec 14, 2011 at 3:21 PM
Edited Dec 14, 2011 at 4:15 PM

I have a problem when I try to set a property "value" more than once if the property has ModelAttribute. The System.ArgumentException is raised with message "An item with the same key has already been added".

[Model]
Person SelectedPerson { get { ...} private set { this.Set(SelectedPersonProperty, value); } }

My workaround is an extension method that copy the object to the registered one as PropertyData.

set { this.CopyClone(SelectedPersonProperty, value); }

What is the correct approach to deal with multiples set calls of properties with ModelAttribute?

Dec 14, 2011 at 3:32 PM

Why would you want to use the Model attribute on the same property multiple times? It should be possible to define multiple models on a view model.

Dec 14, 2011 at 3:50 PM
Edited Dec 14, 2011 at 4:27 PM

Sorry my mistake (I modified the original post)

I want set the property value multiples times.

I need to do this:

var personListViewModel = new PersonListViewModel();
personListViewModel.SelectedPerson = person1; personListViewModel.SelectedPerson = person2; // The exception is raised here!!! personListViewModel.SelectedPerson = null; personListViewModel.SelectedPerson = person3;

but when the property have ModelAttribute, the set call fail, raising the exception described before.

What is the correct approach to deal with multiples "set calls" of properties with ModelAttribute?

Dec 14, 2011 at 7:58 PM

I have seen this problem before:

I had made a UI that has a list of persons from which the user can select and that then shows the details of that person (and allows to edit them). I had crammed this into 1 single view with 1 single viewmodel. For the first selected person all was ok, but it blew up with that same exception when selecting the second person.

I changed my "design" and split up the thing into 2 views and 2 viewmodels.

- 1 view + viewmodel for the list of persons and handling the SelectedPerson property

- 1 view + viewmodel for the person details (user control that can be put in the selection view above + reused in a datawindow for editing the data

(see the structure of the Person sample of Catel).

Dec 15, 2011 at 3:41 PM

It looks as the correct approach.  The catel feature that supports it, is well described on the documentation  (plus the master/details sample).

 - http://catel.catenalogic.com/how_can_i_inject_or_manipulate.htm

 - http://catel.catenalogic.com/advanced_usercontrol.htm

I'm really have divided the views and the view models.

My mistake was attempt to do something like this

<PersonDetailsView Person="{Binding SelectedPerson}" />

instead

 <PersonDetailsView  DataContext="{Binding SelectedPerson}" />

Thanks.

Dec 15, 2011 at 3:42 PM

So if I understand correctly, the issue is solved for you?

Dec 15, 2011 at 3:55 PM
Edited Dec 15, 2011 at 4:06 PM

Yes,

but Null values do not report property changes.

I don't really know how deal with Null values.

So, my workaround is use an instance that works as empty one, just like this "Person.Empty".

Do you know how deal with null values?

Dec 15, 2011 at 4:48 PM

When the DataContext becomes null, the view model is closed. As soon as a valid person object is loaded again, the view model will be constructed for the newly selected person.

The person example allows you to deselect the current person (using CTRL + click on the selected item), and then the property becomes null.

What kind of property change would you expect?

Dec 15, 2011 at 5:30 PM
Edited Dec 15, 2011 at 5:49 PM

I have an small design problem to solve nested model problems.

On the next example code, the property HasHeir is bound to a CheckBox.  Checked it cause the initialization of the nested model, Unchecked it set the heir model back to null.

If I understand, I need to move down Heir model to another viewmodel to deal with it correctly. Isn't it?

// Looks as very incorrect approach :( 
class PersonDetailsViewModel
{
    PersonViewModel(Person person)
    {
        this.Person = person; 
    }

    [Model]
    public Person Person;

    [ViewModelToModel("Person")]
    [Model]
    public Person Heir { get {...}  set {...} }
    
   publc bool HasHeir
   {
       get { return this.Heir != null; }
       set { if (value) { this.Heir = new Person(...); } } // This code cause the exeption described before.
   }
}
 
Dec 15, 2011 at 7:39 PM

Officially, there are no unit tests for this behavior, and we normally recommend to have one model per view model, but this is possible. So you are not doing anything wrong, as long as you as a developer can keep grip on what you are doing.

Dec 16, 2011 at 6:07 PM

I know, but I'm trying to use the Catel features to the max.

I'm also thought that the approach above was correct, but I can't set the value of model property more than ones, even with only one model property.

Allow me modify the example above with a simple one.

public class PersonDetailViewModel : ViewModelBase 
{
  [Model]
  public Person { get ...; set ...; }

  [Model]
  [ViewModelToMode("Person")]
  public ContacInfo { get ... ;  private set ... }
  
  [ViewModelToModel("ContactInfo")]
  public string Email { get...; private set... } 
  
  publc bool HasContactInfo
  {
       get { return this.ContactInfo != null; }
       set { if (value) { this.ContactInfo = new ContactInfo(...); } else { this.ContactInfo = null; } }  // It's fail when a new instance of contact info is created. 
   }
}

Now if I remove the ModelAttribute on ContactInfo property everything works, but I need copy manually the email value, otherwise the System.ArgumentException is raised with message "An item with the same key has already been added".

Dec 17, 2011 at 11:02 AM

This unit test succeeds now:

[TestMethod]
public void ViewModelWithViewModelToModelMappings_DoubleModels()
{
    var firstPerson = new Person();
    firstPerson.FirstName = "John";
    firstPerson.LastName = "Doe";
    firstPerson.ContactInfo.Street = "Unknown street";
    firstPerson.ContactInfo.City = "Unknown city";
    firstPerson.ContactInfo.Email = "john@doe.com";

    var secondPerson = new Person();
    secondPerson.FirstName = "Second";
    secondPerson.LastName = "Person";
    secondPerson.ContactInfo.Street = "Another street";
    secondPerson.ContactInfo.City = "Another city";
    secondPerson.ContactInfo.Email = "Another email";            

    var viewModel = new MultipleModelMappingsViewModel(firstPerson);

    Assert.IsNotNull(viewModel.Person);
    Assert.IsNotNull(viewModel.ContactInfo);
    Assert.AreEqual("john@doe.com", viewModel.Email);

    viewModel.Person = secondPerson;

    Assert.IsNotNull(viewModel.Person);
    Assert.IsNotNull(viewModel.ContactInfo);
    Assert.AreEqual("Another email", viewModel.Email);
}

The view model used in this testing scenario looks like this:

public class MultipleModelMappingsViewModel : ViewModelBase
{
	public MultipleModelMappingsViewModel(IPerson person)
	{
		Person = person;
	}

	/// <summary>
	/// Gets or sets the person.
	/// </summary>
	[Model]
	public IPerson Person
	{
		get { return GetValue<IPerson>(PersonProperty); }
		set { SetValue(PersonProperty, value); }
	}

	/// <summary>
	/// Register the Person property so it is known in the class.
	/// </summary>
	public static readonly PropertyData PersonProperty = RegisterProperty("Person", typeof(IPerson));

	/// <summary>
	/// Gets or sets the contact info.
	/// </summary>
	[Model]
	[ViewModelToModel("Person")]
	public IContactInfo ContactInfo
	{
		get { return GetValue<IContactInfo>(ContactInfoProperty); }
		set { SetValue(ContactInfoProperty, value); }
	}

	/// <summary>
	/// Register the ContactInfo property so it is known in the class.
	/// </summary>
	public static readonly PropertyData ContactInfoProperty = RegisterProperty("ContactInfo", typeof(IContactInfo));

	/// <summary>
	/// Gets or sets the email.
	/// </summary>
	[ViewModelToModel("ContactInfo")]
	public string Email
	{
		get { return GetValue<string>(EmailProperty); }
		set { SetValue(EmailProperty, value); }
	}

	/// <summary>
	/// Register the Email property so it is known in the class.
	/// </summary>
	public static readonly PropertyData EmailProperty = RegisterProperty("Email", typeof(string));
}

 

I hope this is exactly what you mean.

Dec 17, 2011 at 2:27 PM

No, when running the SL unit tests, it fails with the exception. Now I have a clue where to search for, didn't realize that you were using SL.

Dec 17, 2011 at 3:24 PM

Also see the third reply (from me, GEERTD) on how you can also reproduce it (was also in SL): modify the Person sample by merging the 2 vm into 1 and the 2 views into 1. The first person selected works, the second selection does not work.

Dec 17, 2011 at 3:59 PM
Edited Dec 17, 2011 at 4:00 PM

Issue is fixed, I have updated the beta. You know where to find it ;)

We now officially support ViewModelToModel attributes decorated with the Model attribute again :)