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.
Related
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.
I've never create unit testing before. I'm planning to create UI test & Unit test for my presenter & datasource. I use Retrofit, RxJava, and Dagger in my apps.
Here's what i've tried so far
DataSource (My Datasource is coming from API)
public class DataSource implements DataSourceContract {
private static DataSource dataSource;
#Inject
SharedPreferences sharedPreferences;
#Inject
NewsService newsService;
private DataSource(Context context) {
DaggerAppComponent.builder()
.networkModule(new NetworkModule(API_URL))
.appModule(new AppModule(context.getApplicationContext()))
.preferencesModule(new PreferencesModule())
.build()
.inject(this);
}
public static synchronized DataSource getInstance(Context context) {
if(dataSource == null) {
dataSource = new DataSource(context);
}
return dataSource;
}
public String parseError(Throwable e) {
if(e instanceof SocketTimeoutException) {
return ERROR_TIMEOUT;
}
else if(e instanceof SocketException) {
return ERROR_NO_CONNECTION;
}
else {
return ERROR_SERVER;
}
}
#Override
public DisposableObserver<NewsResponse> getNews(final Callback<NewsResponse> callback) {
return newsService.getNews()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(new DisposableObserver<NewsResponse>() {
#Override
public void onNext(NewsResponse value) {
callback.onSuccess(value);
}
#Override
public void onError(Throwable e) {
callback.onFailure(e);
}
#Override
public void onComplete() {
}
});
}
}
Presenter
public class MainPresenter implements MainContract.Presenter {
private MainContract.View view;
private DataSource dataSource;
private Disposable dispossable;
public MainPresenter(MainContract.View view, DataSource dataSource) {
this.view = view;
this.dataSource = dataSource;
}
#Override
public void onStart() {
getNews();
}
#Override
public void onStop() {
if(dispossable != null && !dispossable.isDisposed()) {
dispossable.dispose();
}
}
#Override
public void getNews() {
view.setLoading(true);
dispossable = dataSource.getNews(new DataSourceContract.Callback<NewsResponse>() {
#Override
public void onSuccess(NewsResponse responseData) {
try {
switch (responseData.getStatus()) {
case API_SUCCESS:
view.setLoading(false);
view.getNewsSuccess(responseData.getArticles());
break;
default:
view.setLoading(false);
view.getNewsFailed(responseData.getStatus());
break;
}
}
catch (Exception e) {
view.setLoading(false);
view.getNewsFailed(ERROR_SERVER);
}
}
#Override
public void onFailure(Throwable e) {
view.setLoading(false);
view.isNetworkFailed(dataSource.parseError(e), false);
}
});
}
}
And this is the test of my presenter
public class MainPresenterTest {
#Mock
DataSource dataSource;
#Mock
MainContract.View view;
MainContract.Presenter presenter;
#Before
public void setup() {
MockitoAnnotations.initMocks(this);
presenter = new MainPresenter(view, dataSource);
}
#Test
public void getNews() throws Exception {
List<Article> articleList = new ArrayList<>();
presenter.getNews();
Mockito.verify(view, Mockito.only()).getNewsSuccess(articleList);
}
}
But there is error when I run the test
Wanted but not invoked:
view.getNewsSuccess([]);
-> at com.java.mvp.view.main.MainPresenterTest.getNews(MainPresenterTest.java:37)
I have no problem running this apps on the device, but I can't make it work on testing
Any idea how to fix this presenter test? Am I doing it right?
And how do I test my datasource? I have no idea how to test this one
Thank you
Keep things simple. You are testing your presenter, not the data source. Add new methods to your presenter for the success and error responses. Then add two tests: one for the success and one for the error.
#Override
public void getNews() {
view.setLoading(true);
dispossable = dataSource.getNews(new DataSourceContract.Callback<NewsResponse>() {
#Override
public void onSuccess(NewsResponse responseData) {
onSuccessNewsResponse(responseData);
}
#Override
public void onFailure(Throwable e) {
onErrorNewsResponse(e);
}
});
}
Add #VisibleForTesting annotation to the new methods.
Success test:
#Test
public void getNewsSuccess() {
presenter.onSuccessNewsResponse(your_response);
Mockito.verify(...);
}
Error test:
#Test
public void getNewsError() {
presenter.onErrorNewsResponse(your_error);
Mockito.verify(...);
}
You have to mock also :
dataSource.getNews() using Mockito when :
e.g.
when(dataSource.getNews()).thenReturn(new SuccessCallback());
So you have to lead your test code into the success callback and check there what methods are called.
The same goes with the eroor case.
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
What I have:
I have a custom class MyEntry derived from Xamarin.Forms.Entry and custom renderer classes MyEntryRenderer for Android and iOS.
What I want:
I want to change the keyboard's "enter"-button to a "search"-button by changing ImeOptions on Android and ReturnKeyType on iOS (see sample code). When I press the altered "search"-button, the MyEntry.Completed event should be called (like before when I pressed the un-altered "enter"-button.
What really happens:
On iOS the code works like expected. But on Android nothing happens. The event doesn't get called.
My question:
How can I achieve what I described above on Android?
Sample code:
App.cs:
namespace CustomEntry
{
public class App
{
public static Page GetMainPage()
{
MyEntry entry = new MyEntry {
VerticalOptions = LayoutOptions.CenterAndExpand,
HorizontalOptions = LayoutOptions.CenterAndExpand,
Placeholder = "Enter some text"
};
entry.Completed += delegate {
Console.WriteLine("Completed");
};
return new ContentPage {
Content = entry,
};
}
}
}
MyEntry.cs:
namespace CustomEntry
{
public class MyEntry:Entry
{
}
}
MyEntryRenderer.cs (Android):
[assembly: ExportRenderer(typeof(MyEntry), typeof(MyEntryRenderer))]
namespace CustomEntry.Android
{
public class MyEntryRenderer:EntryRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
{
base.OnElementChanged(e);
if (Control != null) {
Control.ImeOptions = global::Android.Views.InputMethods.ImeAction.Search;
}
}
}
}
MyEntryRenderer.cs (iOS):
[assembly: ExportRenderer(typeof(MyEntry), typeof(MyEntryRenderer))]
namespace CustomEntry.iOS
{
public class MyEntryRenderer:EntryRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
{
base.OnElementChanged(e);
if (Control != null) {
Control.ReturnKeyType = UIReturnKeyType.Search;
}
}
}
}
I found a workaround for my problem myself:
First I added an Action to my custom entry to be called when I press my "search"-button.
MyEntry.cs
namespace CustomEntry
{
public class MyEntry:Entry
{
public Action SearchPressed = delegate {
};
}
}
Second I "listen" for ImeAction.Search like this and call the Action I added to my custom entry.
MyEntryRenderer.cs (Android):
[assembly: ExportRenderer(typeof(MyEntry), typeof(MyEntryRenderer))]
namespace CustomEntry.Android
{
public class MyEntryRenderer:EntryRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
{
base.OnElementChanged(e);
if (Control != null) {
Control.ImeOptions = ImeAction.Search;
Control.EditorAction += (sender, args) => {
if (args.ActionId == ImeAction.Search) {
var entry = (AddressEntry)Element;
entry.SearchPressed();
}
};
}
}
}
}
In a third class where I use MyEntry I can run some code when the "search"-button is pressed like this:
var myEntry = new MyEntry();
myEntry.SearchPressed += SomeMethod;
public class EntryExtensionRenderer : EntryRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs e)
{
base.OnElementChanged(e);
if ((Element as EntryExtension).NoSuggestionsKey)
{
Control.AutocorrectionType = UITextAutocorrectionType.No;
}
if ((Element as EntryExtension).ReturnKeyType.Equals("Done"))
{
this.AddDoneButton("Done", (EntryExtension)Element);
}
else if ((Element as EntryExtension).ReturnKeyType.Equals("Next"))
{
this.AddDoneButton("Next", (EntryExtension)Element);
}
}
protected void AddDoneButton(string button, EntryExtension entry)
{
UIToolbar toolbar = new UIToolbar(new RectangleF(0.0f, 0.0f, 50.0f, 44.0f));
var doneButton = new UIBarButtonItem();
if (button.Equals("Done")) {
doneButton = new UIBarButtonItem(UIBarButtonSystemItem.Done, delegate
{
entry.KeyPressedEnter();
});
}
if (button.Equals("Next"))
{
doneButton = new UIBarButtonItem("Next", UIBarButtonItemStyle.Done, delegate
{
entry.KeyPressedEnter();
});
}
toolbar.Items = new UIBarButtonItem[] {
new UIBarButtonItem (UIBarButtonSystemItem.FlexibleSpace),
doneButton
};
this.Control.InputAccessoryView = toolbar;
}
}
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.