MVVMCross: Binding Two Way doesn't update viewmodel - android

I'm trying to bind a property in a custom control in a Xamarin.Android project.
public class MyControl : RelativeLayout
{
public ObservableCollection<string> MyProperty { get; set; }
}
When MyProperty is updated in the ViewModel side, it updates fine the MyProperty in the View. However, nothing happens if I update the MyProperty in the View (I'd like to get the updated value in the ViewModel).
The binding:
public class MyControlMyPropertyTargetBinding : MvxAndroidTargetBinding
{
private bool _subscribed;
protected MyControl MyControl
{
get { return (MyControl)Target; }
}
public MyControlMyPropertyTargetBinding(MyControl target)
: base(target)
{
}
protected override void SetValueImpl(object target, object value)
{
var myControl = (MyControl)target;
myControl.MyProperty = (ObservableCollection<string>)value;
}
public override Type TargetType
{
get { return typeof(ObservableCollection<string>); }
}
public override MvxBindingMode DefaultMode
{
get { return MvxBindingMode.TwoWay; }
}
public override void SubscribeToEvents()
{
base.SubscribeToEvents();
var myControl = MyControl;
if (myControl == null || myControl.MyProperty == null)
return;
myControl.MyProperty.CollectionChanged += MyPropertyOnCollectionChanged;
_subscribed = true;
}
private void MyPropertyOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs notifyCollectionChangedEventArgs)
{
FireValueChanged(MyControl.MyProperty);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (isDisposing)
{
var myControl = MyControl;
if (myControl != null && myControl.MyProperty!= null && _subscribed)
{
myControl.MyProperty.CollectionChanged -= MyPropertyOnCollectionChanged;
_subscribed = false;
}
}
}
}
Setup.cs:
protected override void FillTargetFactories(IMvxTargetBindingFactoryRegistry registry)
{
registry.RegisterCustomBindingFactory<MyControl>("MyProperty", myProperty => new MyControlMyPropertyTargetBinding(myProperty));
}
protected override IList<Assembly> AndroidViewAssemblies
{
get
{
var assemblies = base.AndroidViewAssemblies;
assemblies.Add(typeof(MyControl).Assembly);
return assemblies;
}
}
Update: The same control binded on Windows Phone works two way perfectly. The control itself is into an external reference.
Does someone know what I missed?
Edit 1: I updated target binding with collection changed subscription but nothing's fired. The ObservableCollection is updated programatically by code, not by user input.

Related

Porting View Renderer in .net

I am currently migrating my Xamarin.Forms app to .NET MAUI, and having a difficulty in migrating view renderer. In .NET MAUI I am using camera2 in my app, and using the renderer for same.
My Xamarin forms code is
public class CameraRecordV3 : View
{
public static readonly BindableProperty StartProperty = BindableProperty.Create(
"Start", typeof(int), typeof(int), 6000);
public int Start
{
set { SetValue(StartProperty, value); }
get { return (int)GetValue(StartProperty); }
}
}
using iVue.Views;
using System.ComponentModel;
using Microsoft.Maui.Controls.Platform;
using Microsoft.Maui.Controls.Handlers.Compatibility;
namespace iVue.Platforms.Android.Renderers;
public class CameraRecordRenderer_V3 : ViewRenderer<CameraRecordV3, CameraRecordControl_V3>
{
private CameraRecordControl_V3 _cameraControl;
private DisplayTimeHelper _displayTimeHelper = new DisplayTimeHelper();
public CameraRecordRenderer_V3(Context context)
: base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<CameraRecordV3> e)
{
base.OnElementChanged(e);
if (Control == null)
{
_cameraControl = new CameraRecordControl_V3(Context, e.NewElement);
SetNativeControl(_cameraControl);
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
var model = (CameraRecordV3)sender;
base.OnElementPropertyChanged(sender, e);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
_cameraControl.Dispose();
if(Control != null)
Control.Dispose();
}
}
}
CameraRecordControl_V3 is a viewgroup which contains a native view for android, which contains buttons and camera
public class CameraRecordControl_V3 : ViewGroup
{
public CameraRecordControl_V3(Context context, CameraRecordV3 vm) : base(context)
{
_activity = this.Context as Activity;
_view = _activity.LayoutInflater.Inflate(Resource.Layout.CameraRecordLayoutV2, this, false);
AddView(_view);
_toolbar = (Toolbar)_view.FindViewById(Resource.Id.toolbar);
textureView = (AutoFitTextureView)_view.FindViewById(Resource.Id.textureview)
_questionTitleView = (Button)_view.FindViewById(Resource.Id.Start);
}
}
I tried using handler in .net maui but no luck with it.
My Maui Code is as follows
public interface ICameraRecordV3 : IView
{
public int StartTime { get; }
}
public partial class CameraRecordV3Handler
{
public static PropertyMapper<ICameraRecordV3, CameraRecordV3Handler> CustomMapper
= new PropertyMapper<ICameraRecordV3, CameraRecordV3Handler>(ViewHandler.ViewMapper)
{
[nameof(ICameraRecordV3.StartTime)] = MapStartTime,
};
public CameraRecordV3Handler() : base(CustomMapper)
{
}
public CameraRecordV3Handler(PropertyMapper mapper = null) : base(mapper ?? CustomMapper)
{
}
}
public class CameraRecordV3 : View, ICameraRecordV3
{
public static readonly BindableProperty StartProperty = BindableProperty.Create(
"StartTime", typeof(int), typeof(int), 6000);
public int Start
{
set { SetValue(StartTimeProperty, value); }
get { return (int)GetValue(StartTimeProperty); }
}
}
//Platform Specific code
public partial class CameraRecordV3Handler : ViewHandler<ICameraRecordV3, CameraRecordControl_V3>
{
private CameraRecordControl_V3 _cameraControl;
protected override CameraRecordControl_V3 CreatePlatformView()
{
_cameraControl = new CameraRecordControl_V3(Context, null);
return _cameraControl;
}
protected override void ConnectHandler(CameraRecordControl_V3 platformView)
{
base.ConnectHandler(platformView);
}
private static void MapStartTime(CameraRecordV3Handler handler, ICameraRecordV3 arg2)
{
handler.PlatformView?.UpdateStartTime(arg2.StartTime);
}
}
//MauiProgram
builder.ConfigureMauiHandlers(handlers =>
{
#if __ANDROID__
handlers.AddHandler(typeof(CameraRecordV3), typeof(iVue.Handlers.CameraRecordV3Handler));
#endif
});
You can continue to use CustomRenderer in MAUI, you just need to Remove any ExportRenderer directives as they won't be needed in .NET MAUI. And then configure each renderer using conditional compilation for each platform. You can replace handlers.AddCompatibilityRenderer with handlers.AddHandler in the documentation. Using handlers.AddCompatibilityRenderer will cause a crash.

Xamarin Forms : disposed object problem with an Android renderer

Using an Android renderer for a Frame inside a page in Xamarin Forms, I need to change the position of this object after the size allocation of the page.
The page being in a tab in a Shell, when I change tabs and I come back I get the exception 'Cannot access a disposed object' in the renderer.
The exception occurs on this line of UpdatePos:
SetY(20);
My problem has been reproduced with the code below :
The page :
public partial class TestPage : ContentPage
{
public partial class Container : Frame
{
public delegate void PosChangedEvent();
public event PosChangedEvent HandlerPosUpdated;
public void Update()
{
HandlerPosUpdated?.Invoke();
}
}
Container _container = null;
public TestPage()
{
InitializeComponent();
_container = new Container()
{
Content = new myView()
};
main_layout.Children.Add(_container);
}
protected override void OnSizeAllocated(double width, double height)
{
base.OnSizeAllocated(width, height);
_container.Update();
}
}
The renderer :
public class ContainerRenderer : ViewRenderer<Frame, Android.Views.View>
{
public ContainerRenderer(Context context) : base(context)
{}
public void UpdatePos()
{
SetY(20); // System.ObjectDisposedException: 'Cannot access a disposed object. Object name: 'ContainerRenderer'.'
}
protected override void OnElementChanged(ElementChangedEventArgs<Frame> e)
{
base.OnElementChanged(e);
if (e.NewElement != null)
{
TestPage.Container view = e.NewElement as TestPage.Container;
if (view != null)
{
view.HandlerPosUpdated += UpdatePos;
}
}
if (e.OldElement != null)
{
TestPage.Container view = e.OldElement as TestPage.Container;
if (view != null)
{
view.HandlerPosUpdated -= UpdatePos;
}
}
}
}
How this exception could be avoided ?
Any hints are welcome!
Remove that handler when the custom renderer is disposed:
private bool disposedValue;
protected override void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
RemoveHandlerPosUpdated();
}
disposedValue = true;
}
base.Dispose(disposing);
}
private void RemoveHandlerPosUpdated()
{
if (Element != null)
{
TestPage.Container view = Element as TestPage.Container;
if (view != null)
{
view.HandlerPosUpdated -= UpdatePos;
}
}
}
If that doesn't fix it, then may need to do something in TestPage.Container class, to remove any handlers attached to HandlerPosUpdated. Details TBD.

How to update UI with network error response in MVVM using NetworkBoundResource

I'm trying to implement MVVM pattern usingGoogle's android architecture components while using RX Java in NetworkBoundResource. However I'm having a difficult time finding a way to communicate the error response from network call to activity.
here is a link to the original github project.
I have read this post about "Refactoring google's NetworkBoundResource class to use RxJava instead of LiveData" but still not clear how to actually solve the problem.
would appreciate if you could direct me to a code based solution for this scenario for better understanding.
cheers!
GithubRepository:
#Singleton
public class GithubRepository {
private GithubDao githubDao;
private GithubApiService githubApiService;
public GithubRepository(GithubDao githubDao, GithubApiService githubApiService) {
this.githubDao = githubDao;
this.githubApiService = githubApiService;
}
public Observable<Resource<List<GithubEntity>>> getRepositories(Long page) {
return new NetworkBoundResource<List<GithubEntity>, GithubApiResponse>() {
#Override
protected void saveCallResult(#NonNull GithubApiResponse item) {
List<GithubEntity> repositories = item.getItems();
for (GithubEntity githubEntity : repositories) {
githubEntity.setPage(page);
githubEntity.setTotalPages(item.getTotalCount());
}
githubDao.insertRepositories(repositories);
}
#Override
protected boolean shouldFetch() {
return true;
}
#NonNull
#Override
protected Flowable<List<GithubEntity>> loadFromDb() {
List<GithubEntity> repositories = githubDao.getRepositoriesByPage(page);
return (repositories == null || repositories.isEmpty()) ?
Flowable.empty() : Flowable.just(repositories);
}
#NonNull
#Override
protected Observable<Resource<GithubApiResponse>> createCall() {
return githubApiService.fetchRepositories(QUERY_SORT, QUERY_ORDER, page)
.flatMap(response ->
Observable.just(response.isSuccessful()
? Resource.success(response.body())
: Resource.error("", new GithubApiResponse())));
}
}.getAsObservable();
}
}
.:UPDATE:.
So I can see that the the in NetworkBoundResource the doOnError and onErrorResumeNext are handling the error response. but i don't know how to get this response in the activity. I am having a hard time understanding it.
NetworkBoundResource:
public abstract class NetworkBoundResource<ResultType, RequestType> {
private Observable<Resource<ResultType>> result;
#MainThread
protected NetworkBoundResource() {
Observable<Resource<ResultType>> source;
if (shouldFetch()) {
source = createCall()
.subscribeOn(Schedulers.io())
.doOnNext(apiResponse -> saveCallResult(processResponse(apiResponse)))
.flatMap(apiResponse -> loadFromDb().toObservable().map(Resource::success))
.doOnError(t -> onFetchFailed())
.onErrorResumeNext(t -> {
return loadFromDb()
.toObservable()
.map(data -> Resource.error(t.getMessage(), data));
})
.observeOn(AndroidSchedulers.mainThread());
} else {
source = loadFromDb()
.toObservable()
.map(Resource::success);
}
result = Observable.concat(
loadFromDb()
.toObservable()
.map(Resource::loading)
.take(1),
source
);
}
public Observable<Resource<ResultType>> getAsObservable() {return result;}
protected void onFetchFailed() {}
#WorkerThread
protected RequestType processResponse(Resource<RequestType> response) {return response.data;}
#WorkerThread
protected abstract void saveCallResult(#NonNull RequestType item);
#MainThread
protected abstract boolean shouldFetch();
#NonNull
#MainThread
protected abstract Flowable<ResultType> loadFromDb();
#NonNull
#MainThread
protected abstract Observable<Resource<RequestType>> createCall();
}
GithubListViewModel:
public class GithubListViewModel extends ViewModel {
private Long currentPage = 0l;
private GithubRepository repository;
private List<GithubEntity> repositories = new ArrayList<>();
private SingleLiveEvent<List<GithubEntity>> repoListLiveData = new SingleLiveEvent<>();
#Inject
public GithubListViewModel(GithubDao githubDao, GithubApiService githubApiService) {
repository = new GithubRepository(githubDao, githubApiService);
}
public void fetchRepositories() {
repository.getRepositories(++currentPage)
.subscribe(resource -> {
if(resource.isLoaded()) {
repositories.addAll(resource.data);
getRepositoryListLiveData().postValue(resource.data);
}
});
}
public List<GithubEntity> getRepositories() {
return repositories;
}
public SingleLiveEvent<List<GithubEntity>> getRepositoryListLiveData() {
return repoListLiveData;
}
public boolean isLastPage() {
return getRepositoryListLiveData().getValue() != null &&
!getRepositoryListLiveData().getValue().isEmpty() ?
getRepositoryListLiveData().getValue().get(0).isLastPage() :
false;
}
}
GithubActivity:
public class GithubListActivity extends AppCompatActivity implements RecyclerLayoutClickListener {
...
private void initialiseViewModel() {
githubListViewModel = ViewModelProviders.of(this, viewModelFactory).get(GithubListViewModel.class);
githubListViewModel.getRepositoryListLiveData().observe(this, repositories -> {
if(githubListAdapter.getItemCount() == 0) {
if(!repositories.isEmpty()) {
animateView(repositories);
} else displayEmptyView();
} else if(!repositories.isEmpty()) displayDataView(repositories);
});
}
...
githubListViewModel.fetchRepositories();
...
}

Launching error on Android Mdpi Tablet: black screen shows

Hey I am working on Xamarin and Mvvmcross, my issue is, when I start the app on (Samsung Galaxy Tab 3) Mdpi device then it show black screen and nothing happens,
here is my code,
Splash.cs
namespace Mobile.UI.Droid
{
[Activity(
Label = "Mobile Tasks"
, MainLauncher = true
, Icon = "#drawable/icon"
,Theme = "#style/MyActionBarSplash"
, NoHistory = true)]
public class SplashScreen : MvxSplashScreenActivity
{
public SplashScreen()
: base(Resource.Layout.SplashScreen)
{
}
}
}
setup.cs
public class Setup : MvxAndroidSetup
{
Context _context;
public Setup(Context applicationContext) : base(applicationContext)
{
_context = applicationContext;
CurrentPlatform.Init();
Insights.Initialize(XamarinInsightsConstants.APIKey, applicationContext);
}
protected override IMvxAndroidViewPresenter CreateViewPresenter()
{
var presenter = new CustomDroidViewPresenter();
Mvx.RegisterSingleton(presenter);
return presenter;
}
protected override IMvxApplication CreateApp()
{
return new Core.App();
}
protected override System.Collections.Generic.IList<string> ViewNamespaces
{
get
{
var toReturn = base.ViewNamespaces;
toReturn.Add("MOBILE.Mobile.UI.Droid.Controls");
toReturn.Add("MOBILE.Mobile.UI.Droid.Utilities");
return toReturn;
}
}
protected override IMvxTrace CreateDebugTrace()
{
return new DebugTrace();
}
protected override void InitializeLastChance()
{
var errorHandler = new ErrorDisplayer(ApplicationContext);
//Cirrious.MvvmCross.Plugins.Color.PluginLoader.Instance.EnsureLoaded();
base.InitializeLastChance();
Mvx.RegisterSingleton<IDeviceDetails>(new CustomDroidDetails());
//Mvx.RegisterSingleton<ILogger>(new DroidLogger());
Mvx.RegisterSingleton<IVersionDetail>(new VersionDetail());
}
protected override void FillTargetFactories(IMvxTargetBindingFactoryRegistry registry)
{
base.FillTargetFactories(registry);
registry.RegisterFactory(new MvxCustomBindingFactory<LinearLayout>("ShapeBackground", (view) => new ShapeBackgroundBinding(view)));
registry.RegisterFactory(new MvxCustomBindingFactory<TextView>("CustomText", (view) => new CustomTextBinding(view)));
}
}
VersionDetail.cs
public class VersionDetail : IVersionDetail
{
public string GetAppVersion()
{
var version = Application.Context.PackageManager.GetPackageInfo(Application.Context.PackageName, 0).VersionName;
return version;
}
}
CustomDroidDetails
public class CustomDroidDetails : IDeviceDetails
{
public Platform Platform
{
get { return Platform.Droid; }
}
public DeviceType DeviceType
{
get { return DeviceType.Phone; }
}
}
logcat
Error

MvvmCross Checkbox bind to command android xml

Is it possible to bind android checkbox to execute a command on change? Could not find an example
Standard approach would be to simply bind to property of type bool in your viewmodel and perform your logic in setter of this property. Your binding will then look like this:
local:MvxBind="Checked IsChecked"
However if you really need bind to Command, you can also bind to Click event:
local:MvxBind="Checked IsChecked; Click YourCommand;"
ViewModel:
private bool _isChecked;
public bool IsChecked
{
get { return _isChecked; }
set
{
_isChecked = value;
RaisePropertyChanged(() => IsChecked);
}
}
public ICommand YourCommand
{
get
{
return new MvxCommand(() =>
{
var isChecked = IsChecked;
//Now you can use isChecked variable
});
}
}
Note that you don't recieve value of the checkbox in your command parameter, so you need to bind to the bool property anyway. Another problem with this solution is that you must rely on a fact, that setter of your property would be called before your command.
If you really need to have command with bool parameter, then you can definitely do that. Awesome thing about MvvmCross framework is that you can always extend its functionality. In your case you would need to implement custom binding for CheckBox. Good starting point may be here: http://slodge.blogspot.cz/2013/06/n28-custom-bindings-n1-days-of-mvvmcross.html
Edit: To show how easy it is I gave it a try and implement simple command binding with bool parameter. (No CanExecute check). In case anyone is interested, here is the code.
Binding class:
public class CheckBoxChangedBinding
: MvxAndroidTargetBinding
{
private ICommand _command;
protected CheckBox View
{
get { return (CheckBox) Target; }
}
public CheckBoxChangedBinding(CheckBox view)
: base(view)
{
view.CheckedChange += CheckBoxOnCheckedChange;
}
private void CheckBoxOnCheckedChange(object sender, CompoundButton.CheckedChangeEventArgs e)
{
if (_command == null)
return;
var checkBoxValue = e.IsChecked;
_command.Execute(checkBoxValue);
}
protected override void SetValueImpl(object target, object value)
{
_command = value as ICommand;
}
public override MvxBindingMode DefaultMode
{
get { return MvxBindingMode.OneWay; }
}
public override Type TargetType
{
get { return typeof (ICommand); }
}
protected override void Dispose(bool isDisposing)
{
if (isDisposing)
{
var view = View;
if (view != null)
{
view.CheckedChange -= CheckBoxOnCheckedChange;
}
}
base.Dispose(isDisposing);
}
}
In Setup.cs:
protected override void FillTargetFactories(IMvxTargetBindingFactoryRegistry registry)
{
base.FillTargetFactories(registry);
registry.RegisterCustomBindingFactory<CheckBox>("CheckedChanged",
checkBox => new CheckBoxChangedBinding(checkBox));
}
In your layout:
<CheckBox
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
local:MvxBind="CheckedChanged CheckBoxCheckedCommand" />
And finally ViewModel:
public ICommand CheckBoxCheckedCommand
{
get
{
return new MvxCommand<bool>(isChecked =>
{
var parameter = isChecked;
});
}
}

Categories

Resources