4

In a MvvmCross app, I have a page with the classic chat behavior (WhatsApp like): this page shows the history of messages exchanged between two users with the last message at the bottom of the list. I've successfully implemented the view in Windows Phone 8.1, but I'm struggling with a problem in Android.

I'll give you a short introduction and description of my problem and next I'll go through technical details.

INTRODUCTION

Actually, my need is to apply different style to messages sent by different users: tipically align left messages sent from other user and align right messages sent by me (I do this through the weight property); I need to apply a different drawable background and set different gravity property also. I use custom binding because, AFAIK, those properties cannot be binded with classic binding: local:MvxBind="Gravity MyPropery" doesn't work because there is no Gravity property.

So, I have of course two axml files:

  • the first one contains the Mvx.MvxListView
  • the second one contains the item template for MvxListView

And I've created three different custombinding (for Background, Gravity and Weight) following these guides:

THE PROBLEM

I want that, when a user opens the chat View, the list widget shows automatically the last message. To accomplish this, I scroll programmatically the list to the last message and this seems to be the problem.

If I don't scroll programmatically, when I open the page and scroll manually to the end of the page, all custom bindings are applied successfully: I can see messages aligned right and left, with correct background and weight applied.

If I force the scroll programmatically, when I open the page I see a strange behavior: all the messages are present (classic binding, such as Text property, have been successfully applied), but custom bindings are missing. All the messages have the same background and are all left aligned. BUT, if I scroll manually up and down, the custom binding are processed and the messages are displayed with right style.

DEBUG ANALYSIS

To debug the behaviour I've put a simple static counter in a custom binding procedure to track every time the function is processed.

public class LinearLayoutWeightTargetBinding : MvxAndroidTargetBinding
{
    public static int debugCounter = 0;
    public LinearLayoutWeightTargetBinding(object target) : base(target)
    {
    }

    protected LinearLayout MyTarget
    {
        get { return (LinearLayout)Target; }
    }

    public override Type TargetType { get { return typeof(bool); } }

    protected override void SetValueImpl(object target, object value)
    {
        var ll = (LinearLayout)target;
        var itsMe = (bool)value;
        var weight = itsMe ? (float)20.0 : (float)5.0;

        var layoutParams = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WrapContent, weight);
        ll.LayoutParameters = layoutParams;
        Log.Debug("MeeCHAT", string.Format("LinearLayoutWeightTargetBinding::SetValueImpl::ItsMe:{0} - counter:{1}", itsMe, ++debugCounter));
    }

    public override MvxBindingMode DefaultMode { get {return MvxBindingMode.TwoWay;} }
}

By this way I saw that actually by scrolling up and down the custom bindings are applied (debugCounter increases correctly). BUT when I apply the programmatically scroll, only the first 10 items are processed by the custom bindings and this seems the reason why I see the messages without the right style. Because I have a long list, only the first 10 items are processed but they are not visible (they are out of the visible area) and the visibile items have not been processed.

TECHNICAL DETAILS

Here are some details related to technical aspects of my app. I try to give you all important aspects.

ORGANIZATION OF THE VIEWS By following the approach described by Greg Shackles in this article http://gregshackles.com/presenters-in-mvvmcross-navigating-android-with-fragments/ I have just one general Activity for the app and one Fragment for each View; then through a Presenter is possible to activate the right ViewModel and manage the stack of the navigation.

The Fragment for the View where I have the Mvx.MvxListView widget is

public class MyMatchersChatView : MvxFragment
{
    public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
    {
        var ignore = base.OnCreateView(inflater, container, savedInstanceState);
        var result = this.BindingInflate(Resource.Layout.MyMatchersChatView, null);

        var headerFrame = result.FindViewById<FrameLayout>(Resource.Id.headerFrameMyMatchersChatView);
        var headerWidget = new HeaderWidget() { ViewModel = this.ViewModel };
        var tran = ChildFragmentManager.BeginTransaction();
        tran.Add(headerFrame.Id, headerWidget, "headerMyMatchersChat");
        tran.Commit();

        var listView = result.FindViewById<MvxListView>(Resource.Id.messagesList);
        listView.SetSelection(listView.Adapter.Count - 1); // Scroll to the end of the list
        return result;
    }        
}

The statement listView.SetSelection(listView.Adapter.Count - 1); force the list to scroll to the end.

Last two things: how the custom bindings are registered and how are applied in axml file.

REGISTRATION OF CUSTOM BINDING

In Setup.cs I have:

protected override void FillTargetFactories(IMvxTargetBindingFactoryRegistry registry)
{
    base.FillTargetFactories(registry);
    registry.RegisterFactory(new MvxCustomBindingFactory<LinearLayout>("CustomWeight",
            (b) => new LinearLayoutWeightTargetBinding(b)));
}

APPLYING OF CUSTOM BINDING

In my axml I have:

<LinearLayout 
  android:orientation="horizontal" 
  android:layout_width="0dp" 
  android:layout_height="wrap_content" 
  local:MvxBind="CustomWeight IsCurrentUser">

LISTVIEW AND VIEWMODEL

Here is the code of ListView

<Mvx.MvxListView
  android:id="@+id/messagesList"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  local:MvxBind="ItemsSource MyMessages"
  local:MvxItemTemplate="@layout/mymatcherschatview_itemtemplate" />

and the property in ViewModel

    private IEnumerable<MyMatchMessageModel> _myMessages;
    public IEnumerable<MyMatchMessageModel> MyMessages
    {
        get { return _myMessages; }
        set
        {
            _myMessages = value;
            RaisePropertyChanged(() => MyMessages);
        }
    }

ENVIRONMENT Finally, here is my environment:

  • Visual Studio 2015
  • MvvmCross 3.5.1
  • Core targets: .NET Framework 4.5, Windows 8, ASP.NET Core 5.0, Windows Phone 8.1, Xamarin.Android, Xamarin.iOS, Xamarin.iOS (Classic)
  • The Android app target is API Level 19 (Xamarin.Android v4.4 Support)
  • Xamarin 3.11.1450.0
  • Xamarin.Android 5.1.6.7

Someone can help me to understand if I'm doing something wrong? Thanks for reading and for any help!

>>EDIT 1<<

I've changed my layout by adding stackFromBottom and transcriptMode properties and by removing the scrolling to below programmatically in Fragment obtaining an auto-scroll behavior, but the problem still remains: to see messages with correct style I have to manually scroll up and down (to activate the custom bindings)

Here is the new axml...

<Mvx.MvxListView
  android:id="@+id/messagesList"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  android:stackFromBottom="true"
  android:transcriptMode="alwaysScroll"
  local:MvxBind="ItemsSource MyMessages"
  local:MvxItemTemplate="@layout/mymatcherschatview_itemtemplate" />

...and the new code in Fragment

public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
  var ignore = base.OnCreateView(inflater, container, savedInstanceState);
  var result = this.BindingInflate(Resource.Layout.MyMatchersChatView, null);

  var headerFrame = result.FindViewById<FrameLayout>(Resource.Id.headerFrameMyMatchersChatView);
  var headerWidget = new HeaderWidget() { ViewModel = this.ViewModel };
  var tran = ChildFragmentManager.BeginTransaction();
  tran.Add(headerFrame.Id, headerWidget, "headerMyMatchersChat");
  tran.Commit();

  return result;
}   
Community
  • 1
  • 1
Omar Venturi
  • 91
  • 1
  • 7
  • Use observable collection instead of ienumerable in your view model – xleon Jan 10 '16 at 12:54
  • Hi @xleon , I've already tried your suggestion, but nothing changes. Note, everything works fine with my current code if I have a list with less of 10 items – Omar Venturi Jan 10 '16 at 13:43
  • Its not the number of items but a problem with your custom binding not working until the item gets refreshed I think. Im using a custom mvxadapter instead of custom binding. No issues – xleon Jan 10 '16 at 14:06
  • Did you try list.Invalidate() after binding has been set? – xleon Jan 10 '16 at 14:08
  • Thanks for writing a good question. I don't believe what you are seeing is related to it being a "custom binding". Mvx custom bindings have exactly the same status as built-in bindings. I'd guess that maybe what you are seeing is somehow more related to the behaviour of the Android controls themselves. e.g. in your question example, I'd guess that the `weight` is applied before you scroll - but that it's not until the scroll Android layout until your scrolling causes it to either be recalculated or reapplied (or both). To test this... – Stuart Jan 13 '16 at 07:45
  • To test this... you could look for that MeeCHAT debug trace and/or to use breakpoint - is there trace before you scroll? Or you could also test your "custom binding" theory by doing a custom binding on something simpler like text content - does that work before scrolling? It might also be worth using the hierarchy viewer before and after you scroll to look at what the layout is in each list item. Once you've done that it might give you some insight into how to address the issue... it might give you ideas for how you could trick Android into recalculating... Anyways, that's my "guess". – Stuart Jan 13 '16 at 07:50

1 Answers1

0

First thing I would do is to make sure that your custom binding is always getting called. Set a breakpoint on the SetValueImpl() method and check it´s getting called on those problematic items. If that happens, then the issue relies on the view no getting updated for any reason and you should work on that. If it doesn´t break, you will know for sure it´s a custom binding problem (possibly a bug) in MvxAdapter.

If you find out it´s the second one. I would suggest getting rid of your custom binding and creating your own ChatListAdapter : MvxAdapter as follows:

public class CoolChatListAdapter : MvxAdapter
{
    public CoolChatListAdapter(Context context, IMvxAndroidBindingContext bindingContext) : base(context, bindingContext)
    {
    }

    protected override View GetBindableView(View convertView, object source, int templateId)
    {
        var item = source as MyMatchMessageModel;
        var weight = item.IsCurrentUser ? (float) 20.0 : (float) 5.0;

        var ll = (LinearLayout) convertView;
        var layoutParams = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WrapContent, weight);
        ll.LayoutParameters = layoutParams;

        return base.GetBindableView(convertView, source, templateId);
    }
}

Then, in your android view:

var adapter = new ChatListAdapter(this, (IMvxAndroidBindingContext)BindingContext);
_chatList = FindViewById<MvxListView>(Resource.Id.chat_list_view);
_chatList.Adapter = adapter;
xleon
  • 6,201
  • 3
  • 36
  • 52