Image in TableView (Subtitle) MVVM - uitableview

This may seem like a silly question but I am having a hard time finding the correct answer. I am writing an application using MVVMCross and Xamarin for iOS. In the application i want to use a TableView using the standard Subtitle Cell style. I am able to successfully bind to the title and detail text, but have so far been unsuccessful in binding the Image to a local file. Most examples I see are using images pulled from the web, whereas here i would like to use an image from the Resources folder. I am also trying to avoid having to write a custom cell.
How would you bind a local image to the standard Subtitle Cell style using MVVMCross and Xamarin?
Line of code in question:
var source = new MvxStandardTableViewSource (TableView, UITableViewCellStyle.Subtitle, new NSString( "CellID" ), "ImageURL Image; TitleText Title; DetailText EventDate");
Thanks!

You can do this using a converter:
public class ItemTypeToImageValueConverter : IMvxValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
string assetName = "unknown";
switch ((ItemType)value) {
case ItemType.SOMETHING:
assetName = "something";
break;
default:
throw new ArgumentOutOfRangeException ();
}
return "res:item_" + assetName + ".png";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Then to bind it to your UI:
private readonly MvxImageViewLoader typeImageViewLoader;
public ItemItemTableViewCell(IntPtr handle)
: base(handle)
{
typeImageViewLoader = new MvxImageViewLoader(() => BackgroundImageView);
this.DelayBind(() =>
{
var set = this.CreateBindingSet<ItemItemTableViewCell, Item>();
set.Bind(typeImageViewLoader).To(vm => vm.Type).WithConversion("ItemTypeToImage");
set.Apply();
});
}

Related

Neo4j - Custom converter for field of type List

I am trying to write a custom converter for a nested object so that this object gets saved as string in Neo4j database.
I am using #Convert annotation on my field and passing ImageConverter.class which is my AttributeConverter class.
Everything works fine as expected and I am able to save string representation of Image class in Neo4j db.
However, now instead of single image I want to have List<Image> as my nested field. In this case, putting #Convert(ImageConverter.class) doesn't work.
I see that there is a class called ConverterBasedCollectionConverter which gets used when I have a field of type List<LocalDateTime.
However, I couldn't find any exammples on how to use this class in case of custom converters.
Please can anyone help me with this or if there is any other approach to use custom converter on field of type List.
I am using Neo4j (version 3.4.1) and Spring-data-neo4j (5.0.10.RELEASE) in my application. I am also using OGM.
PS: I am aware that it is advised to store nested objects as separate node establishing a relationship with parent object. However, my use case demands that the object be stored as string property and not as separate node.
Regards,
V
It is not so difficult as I assumed it would be.
Given a class (snippet)
#NodeEntity
public class Actor {
#Id #GeneratedValue
private Long id;
#Convert(MyImageListConverter.class)
public List<MyImage> images = new ArrayList<>();
// ....
}
with MyImage as simple as can be
public class MyImage {
public String blob;
public MyImage(String blob) {
this.blob = blob;
}
public static MyImage of(String value) {
return new MyImage(value);
}
}
and a converter
public class MyImageListConverter implements AttributeConverter<List<MyImage>, String[]> {
#Override
public String[] toGraphProperty(List<MyImage> value) {
if (value == null) {
return null;
}
String[] values = new String[(value.size())];
int i = 0;
for (MyImage image : value) {
values[i++] = image.blob;
}
return values;
}
#Override
public List<MyImage> toEntityAttribute(String[] values) {
List<MyImage> images = new ArrayList<>(values.length);
for (String value : values) {
images.add(MyImage.of(value));
}
return images;
}
}
will print following debug output on save that I think is what you want:
UNWIND {rows} as row CREATE (n:Actor) SET n=row.props RETURN row.nodeRef as ref, ID(n) as id, {type} as type with params {type=node, rows=[{nodeRef=-1, props={images=[blobb], name=Jeff}}]}
especially the images part.
Test method for this looks like
#Test
public void test() {
Actor jeff = new Actor("Jeff");
String blobValue = "blobb";
jeff.images.add(new MyImage(blobValue));
session.save(jeff);
session.clear();
Actor loadedActor = session.load(Actor.class, jeff.getId());
assertThat(loadedActor.images.get(0).blob).isEqualTo(blobValue);
}
I am came up with a solution to my problem. So, in case you want another solution along with the solution provided by #meistermeier, you can use the below code.
public class ListImageConverter extends ConverterBasedCollectionConverter<Image, String>{
public ListImageConverter() {
super(List.class, new ImageConverter());
}
#Override
public String[] toGraphProperty(Collection<Image> values) {
Object[] graphProperties = super.toGraphProperty(values);
String[] stringArray = Arrays.stream(graphProperties).toArray(String[]::new);
return stringArray;
}
#Override
public Collection<Image> toEntityAttribute(String[] values) {
return super.toEntityAttribute(values);
}
}
ImageConverter class just implements AttributeConverter<Image, String> where I serialize and deserialize my Image object to/from json.
I chose to go with this approach because I had Image field in one object and List<Image> in another object. So just by changing #Convert(ListImageConverter.class) to #Convert(ImageConverter.class) I was able to save list as well as single object in Neo4j database.
Note: You can skip overriding toEntityAttribute method if you want. It doesn't add much value.
However you have to override toGraphProperty as within Neo4j code it checks for presence of declared method with name toGraphProperty.
Hope this helps someone!
Regards,
V

MvvmCross - Delayed binding in TableViewCell with value converter - DataContext is not set correctly?

I am relatively new to Mvvm and MvvmCross. I've encountered a problem in iOS;
This is my delayed binding for my MvxTableViewCell-subclass, which I've put in AwakeFromNib:
this.DelayBind(() =>
{
var bindingSet = this.CreateBindingSet<MyTableViewCell, MyViewModel>();
bindingSet.Bind(myLabel).To(vm => vm.Name).WithConversion(debugvalueconverter, base.DataContext);
});
My debugvalueconverter looks like this:
public class DebugValueConverter : IMvxValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
System.Diagnostics.Debug.WriteLine("Converting debug: " + value);
if (parameter != null)
{
var vm = (MyViewModel)parameter;
System.Diagnostics.Debug.WriteLine("Converting debug - name from vm: " + vm.Name);
}
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return new NotImplementedException();
}
}
This generates some odd outputs when I'm starting to recycle cells when dragging in the tableview:
Converting debug: [Name from the cell that's appearing]
Converting debug - name from vm: [Name from an old cell which got recycled]
I.e. the DataContext doesn't seem to be set correctly in this case, BUT the name-property is set correctly. Why is this happening?
My problem is that I want to use a converter that will do some "advanced" calculations according to what's in a few properties of my ViewModel (I.e. my base.DataContext).
Is this a bug in MvvmCross or am I missing something? Why is not the DataContext set correctly here?
Thanks!
I don't think you can send in the view model that way to the cell. You need to do the binding in your view and bind your cell to the collection of objects in your view model from the view. Typically I do something like this:
public partial class MainView : MvxViewController
{
/// <summary>
/// Views the did load.
/// </summary>
/// <summary>
/// Called when the View is first loaded
/// </summary>
public override void ViewDidLoad()
{
base.ViewDidLoad();
var source = new MvxSimpleTableViewSource(MainTableView, MainInspectionCell.Key, MainInspectionCell.Key);
MainTableView.Source = source;
var set = this.CreateBindingSet<MainView, MainViewModel>();
set.Bind(source).To(vm => vm.Inspections);
set.Bind(source).For(s => s.SelectionChangedCommand).To(vm => vm.ItemSelectedCommand);
set.Apply();
MainTableView.ReloadData();
}
And then for the cell I typically do the binding in the constructor of the cell like so:
public partial class MainInspectionCell : MvxTableViewCell
{
public static readonly UINib Nib = UINib.FromName("MainInspectionCell", NSBundle.MainBundle);
public static readonly NSString Key = new NSString("MainInspectionCell");
public MainInspectionCell(IntPtr handle) : base (handle)
{
this.DelayBind(() =>
{
var set = this.CreateBindingSet<MainInspectionCell, Inspection>();
set.Bind(InspectionCell).For(v => v.BackgroundColor).To(vm => vm.StatusColor).WithConversion("NativeColor");
set.Apply();
});
}
public static MainInspectionCell Create()
{
return (MainInspectionCell)Nib.Instantiate(null, null)[0];
}
}
}
Hope this helps! I think the main issue is that you are trying to send the view model directly into the cell which is not there. If that is something you are needing to accomplish you will need to create some kind of wrapper around your collection object which sends in the view model you need to access.

MvvMCross bind command with parameter (in C# code)

How can I bind a command to a button in code in MvvMCross (Xamarin.iOS) with specifying a command parameter?
// command definition
public MvxCommand SaveDealerDataCommand
{
get { return new MvxCommand<bool>(DoSaveDealerDataAction); }
}
public void DoSaveDealerDataAction(bool show)
{
//...
}
// binding
bindingset.Bind(saveButton).To(vm => vm.SaveDealerDataCommand);
Where can I specify the parameter (true/false) that will be passed to the command?
Android and iOS buttons don't have CommandParameter properties in the same way that Windows ones do.
However, MvvmCross did recently introduce a way to introduce CommandParameter bindings via Value Converters - see http://slodge.blogspot.co.uk/2013/06/commandparameter-binding.html
This binding should work as:
bindingset
.Bind(saveButton)
.To(vm => vm.SaveDealerDataCommand)
.WithConversion("CommandParameter", true);
or:
bindingset
.Bind(saveButton)
.To(vm => vm.SaveDealerDataCommand)
.WithConversion(new MvxCommandParameterValueConverter(), true);
Note that this CommandParameter binding isn't completely in the 3.0.8.1 package which is the stable nuget release, so to make this work you may need to either:
Add this manual value converter registration in your Setup.cs
protected override void FillValueConverters(IMvxValueConverterRegistry registry)
{
base.FillValueConverters(registry);
registry.AddOrOverwrite(
"CommandParameter",
new Cirrious.MvvmCross.Binding.MvxCommandParameterValueConverter()
);
}
Or use one of the beta nuget packages uploaded since 3.0.8.1 (set nuget to include prereleases to see these packages).
Or build the source yourself
To achieve your dynamic command parameter using the text in one of your UITextField controls, you could bind the text in that UITextField to a string property on you ViewModel and the code that runs in your button's bound command would be able to access the value via that property when it executes.
In your ViewController, something like:
UITextField textField = new UTextField();
textField.Frame = new RectangleF(0,0,120,30);
Add(textField);
UIButton button = new UIButton();
button.Frame = new RectangleF(70,40,50,30);
button.SetTitle("Click Me");
Add(button);
var bindingSet = this.CreateBindingSet<MyView, MyViewModel>();
bindingSet.Bind(textField).To(vm => vm.StringProperty);
bindingSet.Bind(button).To(vm => vm.ClickCommand);
bindingSet.Apply();
Then, in your ViewModel:
private string _stringProperty = string.Empty;
public string StringProperty
{
get { return _stringProperty; }
set
{
_stringProperty = value;
RaisePropertyChanged(() => StringProperty);
}
}
public ICommand ClickCommand
{
get
{
return new MvxCommand(HandleClick);
}
}
public void HandleClick()
{
//Code that accesses StringProperty (which contains the UITextField's value)
}
To pass a dynamic command parameters to the command in the view model you could create a new class e.g. like this DynamicCommandParameterValueConverter:
/// <summary>
/// This class is inspired by MvvmCross MvxCommandParameterValueConverter,
/// but because we need dynamic command parameters, we need to implement our own combined with a CustomMvxWrappingCommand.
/// </summary>
/// <typeparam name="T">The type of the 'selected item' for the command</typeparam>
/// <typeparam name="TResult">The returned result that is used as input for the command.</typeparam>
public class DynamicCommandParameterValueConverter<T, TResult> : MvxValueConverter<ICommand, ICommand>
{
private readonly Func<T, TResult> commandParameterExpression;
public DynamicCommandParameterValueConverter(Func<T, TResult> commandParameterExpression)
{
this.commandParameterExpression = commandParameterExpression;
}
protected override ICommand Convert(ICommand value, Type targetType, object parameter, CultureInfo culture)
{
return new CustomMvxWrappingCommand<T, TResult>(value, commandParameterExpression);
}
}
The CustomMvxWrappingCommand takes a Func as argument, and is later invoked and passed into the commands CanExecute/Execute method. Here is a snippet of how part of that class could look like:
public void Execute(object parameter)
{
if (wrapped == null)
{
return;
}
if (parameter != null)
{
Mvx.Warning("Non-null parameter overridden in MvxWrappingCommand");
}
wrapped.Execute(commandParameterOverride((T)parameter));
}
You could modify the MvxWrappingCommand class from Mvx to implement the above example.
The use of it all:
set.Bind(myControl).For(control => control.ItemClick).To(vm => vm.MyCommand).WithConversion(
new DynamicCommandParameterValueConverter<MyModel, string>((MyModel item) =>
{
// Do custom logic before parsing the item..
}));

Custom dynamic dataannotation for field visibility c#

I am trying to create a dataannotations attribute that controls field visiblity based on settings in a database. The attribute will be used within a system that will be used by multiple clients. Further, the visibility of the field needs to be able to change on the fly. I know I could do an if statement around each field in the view, but I am trying to avoid that and keep the visibility control within the view model as follows:
[Visible(FirstName)]
public string FirstName { get; set; }
I have tried creating this custom attribute that gets the value from a method from a resource class called ResourceType (which is generated using T4 and contains the necessary code to hit the database):
public class VisibleAttribute : Attribute, IMetadataAware
{
/// <summary>
/// Whether this field is visible
/// </summary>
public bool Hidden { get; set; }
public VisibleAttribute(string theFieldName)
{
ResourceType resources = new ResourceType();
Type _resourceType = typeof(ResourceType);
MethodInfo getHidden = _resourceType.GetMethod("IsHidden");
object[] requiredParams = new object[] { theFieldName };
Hidden = (bool)getHidden.Invoke(resources, requiredParams);
}
public void OnMetadataCreated(ModelMetadata metadata)
{
metadata.ShowForEdit = !Hidden;
metadata.HideSurroundingHtml = Hidden;
}
}
Here is an excerpt of the ResourceType class:
public class ResourceType
{
public const string Creditors_SecondaryCreditorsPayOffYesNo_Require = "Prop_Creditors_SecondaryCreditorsPayOffYesNo_Require";
public static string Prop_FieldName_Require
{
get { return GetHiddenOption(FieldName) ? "true" : "false"; }
}
internal static bool GetHiddenOption(string fieldName)
{
< < Logic here to get the option from the database > >
}
I have also tried the same attribute but with the following constructor:
public VisibleAttribute(string theFieldName)
{
ResourceType resources = new ResourceType();
Type _resourceType = typeof(ResourceType);
PropertyInfo getHidden = _resourceType.GetProperty(theFieldName);
Hidden = (bool)getHidden.GetValue
}
The problem I have with these two attempts is that, since the code is in the constructor, it only runs the first time I load the page after an IIS reset. So, any further changes I make to the visibility settings are not reflected without amother IIS reset.
I also tried creating a custom DataAnnotationsModelMetadataProvider that attempts to only load the setting once per page request:
public class EGTDataAnnotationsModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType,
Func<object> modelAccessor, Type modelType, string propertyName)
{
var data = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
var visibleAttributeMetadata = attributes.SingleOrDefault(a => typeof(VisibleAttribute) == a.GetType());
if (visibleAttributeMetadata != null)
{
VisibleAttribute visibleAttribte = (VisibleAttribute)visibleAttributeMetadata;
if (!visibleAttribte.VisibleIsSet)
{
PropertyInfo getHidden = visibleAttribte.ResourceType.GetProperty("Prop_" + WebUtils.RemoveSectionNameSpace(visibleAttribte.SectionName) + "_" + visibleAttribte.FieldName + "_Hide");
visibleAttribte.IsHidden = bool.Parse(getHidden.GetValue(null, null).ToString());
data.HideSurroundingHtml = visibleAttribte.IsHidden;
data.ShowForEdit = !visibleAttribte.IsHidden;
visibleAttribte.VisibleIsSet = true;
}
else
{
data.HideSurroundingHtml = visibleAttribte.IsHidden;
data.ShowForEdit = !visibleAttribte.IsHidden;
}
}
return data;
}
}
One issue I have with the ModelMetadataProvider is that the CreateMetadata method runs many times for a single field during a single request. It is very inefficient code, and a huge decrease in performace, to call the database 5-10+ times per request to get a setting that has not changed since the beginning of the request. If I try to set a flag indicating I've already loaded the setting, I'm back to the same scenario as above where I don't see the setting change until after an IIS reset.
I'm hoping someone can give me some pointers as to what methods I can employ to see the database changes real time. Or am I trying to do the impossible? Thanks in advance.
You could combine the metadata provider approach with caching the value just that single request.
For this you could use the Items dictionary in the current HttpContext. Be careful with this as a redirect will cause the items to be cleared:
string cacheKey = String.Format("IsVisible-{0}", propertyName)
if(!HttpContext.Current.Items.Contains(cacheKey))
HttpContext.Current.Items[cacheKey] = //get setting from db
bool isVisible = (bool)HttpContext.Current.Items[cacheKey];
You can also consider using the ASP .Net Cache in case that you prefer caching the value not just for the current request (Although you mentioned that within your metadata provider you were trying to load the setting once per request)

EnumConverter in primefaces editable datatable

I wrote an EnumConverter that is described in Use enum in h:selectManyCheckbox? Everything was fine until we recognize that this converter does not work properly in primefaces editable datatable. The problem is that although I added an attribute inside getAsString and getAsObject methods as following:
#Override
public String getAsString(FacesContext context, UIComponent component, Object value) {
if (value instanceof Enum) {
component.getAttributes().put(ATTRIBUTE_ENUM_TYPE, value.getClass());
return ((Enum<?>) value).name();
} else {
throw new ConverterException(new FacesMessage("Value is not an enum: " + value.getClass()));
}
}
public Object getAsObject(FacesContext context, UIComponent component, String value) {
Class<Enum> enumType = (Class<Enum>) component.getAttributes().get(ATTRIBUTE_ENUM_TYPE);
try {
return Enum.valueOf(enumType, value);
} catch (IllegalArgumentException e) {
throw new ConverterException(new FacesMessage("Value is not an enum of type: " + enumType));
}
}
In the latter method(getAsObject) could not find the attribute that I gave to the components attribute map. But out of the pprimefaces editable datatable everything is fine. Is there any solution to achieve this?
This problem is caused because the custom component attribute was not saved in the row state of the PrimeFaces datatable (it works fine in standard h:dataTable).
We're going to need to store this information elsewhere. In the view scope along with the component ID would be one way.
In the getAsString():
context.getViewRoot().getViewMap().put(ATTRIBUTE_ENUM_TYPE + component.getId(), value.getClass());
And in the getAsObject():
Class<Enum> enumType = (Class<Enum>) context.getViewRoot().getViewMap().get(ATTRIBUTE_ENUM_TYPE + component.getId());

Resources