Catel ViewModelToModel not linking - c#

I have a simple ViewModel which has two Models. So it looks like this:
public class ConnectionItemSelectorViewModel : ViewModelBase {
...
#region AvailableConnectionsModel
// Model Nr. 1
[Model]
public ConnectionList AvailableConnectionsModel
{
get { return GetValue<ConnectionList>(AvailableConnectionsModelProperty); }
set { SetValue(AvailableConnectionsModelProperty, value); }
}
public static readonly PropertyData AvailableConnectionsModelProperty = RegisterProperty(nameof(AvailableConnectionsModel), typeof(ConnectionList), () => new ConnectionList());
#endregion
#region SelectedConnectionsModel
// Model Nr. 2
[Model]
public ConnectionList SelectedConnectionsModel
{
get { return GetValue<ConnectionList>(SelectedConnectionsModelProperty); }
set { SetValue(SelectedConnectionsModelProperty, value); }
}
public static readonly PropertyData SelectedConnectionsModelProperty = RegisterProperty(nameof(SelectedConnectionsModel), typeof(ConnectionList), () => new ConnectionList());
#endregion
...
}
ConnectionList extends ModelBase so I can use the [Model]-Attribute several times.
Now I want to expose the properties of the Model to the ViewModel:
public class ConnectionItemSelectorViewModel : ViewModelBase {
...
// init Model properties
#region AvailableConnections
// Using a unique name for the property in the ViewModel
// but linking to the "correct" property in the Model by its name
[ViewModelToModel(nameof(AvailableConnectionsModel), nameof(ConnectionList.Connections))]
public ObservableCollection<ConnectionItem> AvailableConnections
{
get { return GetValue<ObservableCollection<ConnectionItem>>(AvailableConnectionsProperty); }
set { SetValue(AvailableConnectionsProperty, value); }
}
public static readonly PropertyData AvailableConnectionsProperty = RegisterProperty(nameof(AvailableConnections), typeof(ObservableCollection<ConnectionItem>), () => null);
#endregion
// linking other properties to the models
...
}
The problem is that the linking doesn't work. So after initialization the property AvailableConnections (and the others also) are still null, though the Model itself is initialized correctly.
Am I missing something or isn't this possible at all?
thx in advance!

Try setting the MappingType on the ViewModelToModel attribute so that the model wins.

Related

MvvmCross - Passing a string with IMvxNavigationService

I'm currently working on a Xamarin.iOS project that uses a web-api to gather data. However, I'm running into some problems trying to pass the user input from a textfield to the Tableview that gets the result from the api.
To do this I've followed the example on the MvvmCross documentation.
The problem is that the input from the Textfield never reaches the 'Filter' property in my TableviewController's viewmodel. I think I'm not passing the string object correctly to my IMvxNavigationService when called.
To clarify, in my UserinputViewController I'm binding the textfield's text like so:
[MvxFromStoryboard(StoryboardName = "Main")]
public partial class SearchEventView : MvxViewController
{
public SearchEventView (IntPtr handle) : base (handle)
{
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
MvxFluentBindingDescriptionSet<SearchEventView, SearchEventViewModel> set = new MvxFluentBindingDescriptionSet<SearchEventView, SearchEventViewModel>(this);
set.Bind(btnSearch).To(vm => vm.SearchEventCommand);
set.Bind(txtSearchFilter).For(s => s.Text).To(vm => vm.SearchFilter);
set.Apply();
}
}
The Viewmodel linked to this ViewController looks like this:
public class SearchEventViewModel : MvxViewModel
{
private readonly IMvxNavigationService _navigationService;
private string _searchFilter;
public string SearchFilter
{
get { return _searchFilter; }
set { _searchFilter = value; RaisePropertyChanged(() => SearchFilter); }
}
public SearchEventViewModel(IMvxNavigationService mvxNavigationService)
{
this._navigationService = mvxNavigationService;
}
public IMvxCommand SearchEventCommand {
get {
return new MvxCommand<string>(SearchEvent);
}
}
private async void SearchEvent(string filter)
{
await _navigationService.Navigate<EventListViewModel, string>(filter);
}
}
And finally, TableviewController's viewmodel looks like this:
public class EventListViewModel : MvxViewModel<string>
{
private readonly ITicketMasterService _ticketMasterService;
private readonly IMvxNavigationService _navigationService;
private List<Event> _events;
public List<Event> Events
{
get { return _events; }
set { _events = value; RaisePropertyChanged(() => Events); }
}
private string _filter;
public string Filter
{
get { return _filter; }
set { _filter = value; RaisePropertyChanged(() => Filter); }
}
public EventListViewModel(ITicketMasterService ticketMasterService, IMvxNavigationService mvxNavigationService)
{
this._ticketMasterService = ticketMasterService;
this._navigationService = mvxNavigationService;
}
public IMvxCommand EventDetailCommand {
get {
return new MvxCommand<Event>(EventDetail);
}
}
private void EventDetail(Event detailEvent)
{
_navigationService.Navigate<EventDetailViewModel, Event>(detailEvent);
}
public override void Prepare(string parameter)
{
this.Filter = parameter;
}
public override async Task Initialize()
{
await base.Initialize();
//Do heavy work and data loading here
this.Events = await _ticketMasterService.GetEvents(Filter);
}
}
Whenever trying to run, the string object 'parameter' in my TableviewController's Prepare function remains 'null' and I have no idea how to fix it. Any help is greatly appreciated!
I believe the issue is with your command setup
new MvxCommand<string>(SearchEvent);
As this command is being bound to a standard UIButton. It will not pass through a parameter value of your filter but null instead. So the string parameter generic can be removed. Additionally, as you want to execute an asynchronous method I would suggest rather using MvxAsyncCommand
new MvxAsyncCommand(SearchEvent);
Then in terms of SearchEvent method you can remove the parameter. The value of filter is bound to your SearchFilter property. It is this property's value that you want to send as the navigation parameter.
private async Task SearchEvent()
{
await _navigationService.Navigate<EventListViewModel, string>(SearchFilter);
}

Propagate property changes through multiple classes

I'm trying to figure out how to properly pass properties through multiple classes. I know I can just implement INotifyPropertyChanged in each class and listen for changes on the property, but this seems to be quite a lot of unnecessary code.
The situation:
I have a class (let's call it Class1) with two dependency properties: FilterStatement (String) and Filter (Filter class). Setting the statement affects the filter and vice versa.
The conversion logic between statement and filter, however, isn't located in Class1, but in Class3 - which Class1 doesn't know directly. In between there is Class2 which just has to pass through the changes. (You can imagine class 1 to 3 beeing Viewmodel, Model and Repository, though in the real situation this doesn't completly match).
public class Class1
{
public static readonly DependencyProperty FilterProperty = DependencyProperty.Register(
"Filter",
typeof(Filter),
typeof(Class1),
new FrameworkPropertyMetadata(null));
public static readonly DependencyProperty FilterStatementProperty = DependencyProperty.Register(
"FilterStatement",
typeof(String),
typeof(Class1),
new FrameworkPropertyMetadata(null));
public Filter Filter
{
get { return (Filter)GetValue(FilterProperty); }
set { SetValue(FilterProperty, value); }
}
public string FilterStatement
{
get { return (string)GetValue(FilterStatementProperty); }
set { SetValue(FilterStatementProperty, value); }
}
public Class2 MyClass2Instance { get; set; }
}
public class Class2
{
public Class3 MyClass3Instance { get; set; }
public void ChangeClass3Instance(object someParam) {
... // this can change the instance of MyClass3Instance and is called frome somewhere else
// when changed, the new Class3 instance has to get the property values of Class1
}
}
public class Class3
{
private Filter _filter; // here is where the filter set in Class 1 or determined by the statement set in class 1 has to be put
public string MyFilterToStatementConversionMemberFunction(Filter filter)
{
...
}
public Filter MyStatementToFilterConversionMemberFunction(string statement)
{
...
}
}
My naive solution would be to duplicate the properties across all three classes, implement INotifyPropertyChanged in Class2 and Class3 and listen to the changes, propagating everything down to Class3 and in Result back up to Class1. Isn't there a better solution to this?
Actually, while Class1 is not a control (given it is a ViewModel) I don't see a reason to make its properties as DependencyProperty, because implementation of INotifyPropertyChanged should be enough.
However, implementation with DependencyProperty should work as well:
public class Class1
{
public static readonly DependencyProperty FilterProperty =
DependencyProperty.Register(
nameof(Filter),
typeof(Filter),
typeof(Class1),
new PropertyMetadata(OnPropertyChanged));
public static readonly DependencyProperty FilterStatementProperty =
DependencyProperty.Register(
nameof(FilterStatement),
typeof(string),
typeof(Class1),
new PropertyMetadata(OnPropertyChanged));
public Filter Filter
{
get { return (Filter)GetValue(FilterProperty); }
set { SetValue(FilterProperty, value); }
}
public string FilterStatement
{
get { return (string)GetValue(FilterStatementProperty); }
set { SetValue(FilterStatementProperty, value); }
}
public Class2 MyClass2Instance { get; set; }
private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var c = (Class1)d;
if (c.MyClass2Instance?.MyClass3Instance != null)
{
if (e.Property == FilterProperty)
{
c.FilterStatement = c.MyClass2Instance.MyClass3Instance.MyFilterToStatementConversionMemberFunction((Filter)e.NewValue);
}
else if (e.Property == FilterStatementProperty)
{
c.Filter = c.MyClass2Instance.MyClass3Instance.MyStatementToFilterConversionMemberFunction((string)e.NewValue);
}
}
}
}
Please note, Filter.Equals should be implemented properly and conversion methods of the Class3 should return the equal values for equal arguments.
I think you complicate this too much. Observable pattern is better fit to user interface implementation. Why don't you code in Class3 logic which set all propertis in Class1 ? I thnik that overload setter FilterStatmentand Filter in Class1, which is ViewModel is good solution as well. As simpler solution as better.

Passing Parameter From Main to Detail in MVVMCross

I am trying to pass the selected item from the list to the detail view, but myitem is null in the DetailViewmodel even though it is not in the MyViewModel.
MyViewModel.cs
public virtual ICommand ItemSelected
{
get
{
return new MvxCommand<MyViewModel>(item =>{SelectedItem = item;});
}
}
public MyViewModel SelectedItem
{
get { return _selectedItem; }
set
{
_selectedItem = value;
// myItem is NOT null here!!!
ShowViewModel<MyDetailViewModel>(new { date = Date, myItem = _selectedItem });
RaisePropertyChanged(() => SelectedItem);
}
}
MyDetailViewModel.cs
public class MyDetailViewModel: MvxViewModel
{
private MyViewModel _myItem;
public void Init(DateTime date, MyViewModel myItem = null)
{
// myItem is NULL here!!!
_myItem = myItem;
}
}
You can use a parameter object, because you can only pass one parameter. I usually crate a nested class Parameter for this.
public class MyDetailViewModel: MvxViewModel
{
private MyViewModel _myItem;
public class Parameter
{
public DateTime Date {get; set; }
public string Name {get; set;}
}
public void Init(Parameter param)
{
Name = param.Name;
}
}
and show the viewmodel like:
ShowViewModel<MyDetailViewModel>(new MyDetailViewModel.Parameter { Date = Date, Name = _selectedItem.Name });
But be aware!
The paramters cannot be complex due certain platform issues. You might have to pass only the Id of your Item within the Parameter object and then load MyItem in your Init function. Or you pass only a string and use serialization: https://stackoverflow.com/a/19059938/1489968
myItem is null because if you pass typed parameter to Init it should be the only parameter you pass. According to MvvmCross ViewModel Creation documentation:
Init() can come in several flavors:.
individual simply-Typed parameters
a single Typed parameter object with simply-Typed properties
as InitFromBundle() with an IMvxBundle parameter - this last flavor is always supported via the IMvxViewModel interface.

Custom Object resets its values after initializing from constructor

I create (if not exists) a new ViewModel Instance via the IMessenger (MVVM Light Toolkit) and pass a custom Object trough the constructor, which is a Property of my MainViewModel.
In this ViewModel I set it to a Property too and using it e.g. for a Command Execute Method. But when I trigger the Command via Binding, the custom Object Property looses its values.
I already debugged and saw, that the object get's passed correctly with its values, but after initializing from the constructor its empty (Not null, just empty Properties).
My custom Object
public class CustomObject : ObservableObject
{
public int Id { get; set; }
public string Property1 { get; set; }
public string Property2 { get; set; }
// etc...
}
I create the ViewModel like this
public CustomObject CustomObj
{
get { return _customObj; }
set { Set(ref _customObj, value); }
}
_customViewModel = new CustomViewModel(CustomObj, _dataService);
The ViewModel
public CustomViewModel(CustomObject obj, IDataService dataService)
{
_dataService = dataService;
// Here it sets correctly the Object
CustomObj = obj;
}
public CustomObject CustomObj
{
get { return _customObj; }
set { Set(ref _customObj, value); }
}
// Even before the Command is triggered, the Object is already empty
public ICommand SomeCommand => new RelayCommand<string>(async s =>
{
var someThing = await _dataService.GetSomeData(CustomObj.Id);
// Stuff...
}
They are registered in the SimpleIoC Container as well, if that matters.
What could result that?

MVC: Custom, fluent Html Helpers

I'm writing a custom HTML helper to display a Grid and I'm focussing myself on Telerik and other parties for the syntax I would like to use.
I have a model with 3 properties (Name, DateUpdated and DateCreted) and an IEnumerable of that one is passed to my view:
#model IEnumerable<GridPageFolderViewModel>
Then I have my static HtmlHelperExtensions class:
public static class HtmlHelperExtensions
{
#region Grid
public static GridBuilder<TModelEntry> GridFor<TModel, TModelEntry>(this HtmlHelper<TModel> htmlHelper, TModelEntry model)
{
return new GridBuilder<TModelEntry>();
}
#endregion
}
This class does return a GridBuilder which looks as the following:
public class GridBuilder<TModel> : IGridBuilder
{
#region Properties
private string name { get; set; }
#endregion
#region Methods
public GridBuilder<TModel> WithColumns(Action<ColumnBuilder<TModel>> function)
{
return this;
}
internal MvcHtmlString Render()
{
return new MvcHtmlString("This is a value.");
}
#endregion
#region IGridBuilder Members
public GridBuilder<TModel> Name(string name)
{
this.name = name;
return this;
}
#endregion
#region IHtmlString Members
public string ToHtmlString()
{ return Render().ToHtmlString(); }
#endregion
}
And then I have my ColumnBuilder class.
public class ColumnBuilder<TModel>
{
public void Bind<TItem>(Func<TModel, TItem> func)
{
}
}
With all this code into place (nothing is rendered at this point), I can use the following syntax:
#(Html.GridFor(new GridPageFolderViewModel())
.Name("PageOverviewGrid")
.WithColumns(column =>
{
column.Bind(c => c.Name);
column.Bind(c => c.DateCreated);
column.Bind(c => c.DateUpdated);
}
)
The 'problem' here is that I need to specify the type of object that a single item in the Grid is holding (the GridPageFolderViewModel), otherwise, I cannot access the properties in the column binder code.
Anyone has some advice on how I can get rid of it?
As the view model is IEnumerable<GridPageFolderViewModel>, you just need to declare your helper like this:
public static GridBuilder<TModelEntry> GridFor<TModelEntry>(this HtmlHelper<IEnumerable<TModelEntry>> htmlHelper)
{
return new GridBuilder<TModelEntry>();
}
It would mean your helper can only be used on views where the model is an IEnumerable<T>, but I think that makes sense.
Now in your view you can do:
#(Html.GridFor()
.Name("PageOverviewGrid")
.WithColumns(column =>
{
column.Bind(c => c.Name);
column.Bind(c => c.DateCreated);
column.Bind(c => c.DateUpdated);
}
)
PS. Don't worry if you need to access the model inside your helper. You can still retrieve it from htmlHelper.ViewData.Model, which will be nicely typed as IEnumerable<TModelEntry>
Hope it helps!

Categories

Resources