MvvmCross custom binding display dialog - android

The goal is to display dialog for user to select date on tap on EditText.
I'm truing to implement binding that will show dialog on click. The code is the following:
public class EditDateBinding : BindingWrapper<EditText, DateTime>
{
public EditDateBinding(EditText androidControl) : base(androidControl)
{
}
public override void SubscribeToEvents()
{
Target.Click += InputClick;
}
private void InputClick(object sender, EventArgs args)
{
DateTime parsedDate = DateTime.Now;
DateTime.TryParse(Target.Text, CultureInfo.CurrentCulture, DateTimeStyles.None, out parsedDate);
var dialog = new DatePickerDialogFragment(Target.Context, parsedDate, OnDateSet);
dialog.Show(
// Can't get fragment manager here
, "date");
}
private void OnDateSet(object sender, DatePickerDialog.DateSetEventArgs e)
{
SetValueToView(Target, e.Date);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (isDisposing)
{
if (Target != null)
{
Target.Click -= InputClick;
}
}
}
protected override void SetValueToView(EditText androidControl, DateTime value)
{
androidControl.Text = value.ToShortDateString();
}
}
But I cant find a way to get FragmentManager instance in order to call Show method of instantiated dialog. Can this be implemented in any way?

Found a way to implement it:
var act = (Activity) Target.Context;
dialog.Show(act.FragmentManager, "date");

Related

Recyclerview data disappears when device is rotated

Even though I am using ViewModel, whenever the device is rotated, the data in the Recyclerview disappears. I had to put the makeSearch() method inside the onClick() method because I need to get the text that the button grabs and use it as the search parameter. Is there a better way I can handle this to avoid this problem? My code is right here:
SearchActivity:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_search);
// What happens when the search button is clicked
materialButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (Objects.requireNonNull(textInputEditText.getText()).toString().isEmpty()) {
textInputEditText.setError("Type a search query");
} else {
mSearchInput = Objects.requireNonNull(textInputEditText.getText()).toString();
textInputEditText.setText("");
makeSearch();
}
}
});
}
// Gets the ViewModel, Observes the Question LiveData and delivers it to the Recyclerview
private void makeSearch() {
final SearchAdapter searchAdapter = new SearchAdapter();
SearchViewModel mSearchViewModel = new ViewModelProvider(this,
new CustomSearchViewModelFactory(new SearchRepository())).get(SearchViewModel.class);
mSearchViewModel.setQuery(mSearchInput);
mSearchViewModel.getQuestionLiveData().observe(this, new Observer<List<Question>>() {
#Override
public void onChanged(List<Question> questions) {
mQuestions = questions;
searchAdapter.setQuestions(questions);
}
});
mRecyclerView.setAdapter(searchAdapter);
searchAdapter.setOnClickListener(mOnClickListener);
}
SearchViewModel:
public class SearchViewModel extends ViewModel {
private SearchRepository mSearchRepository;
private MutableLiveData<String> mSearchLiveData = new MutableLiveData<>();
private LiveData<List<Question>> mQuestionLiveData = Transformations.switchMap(mSearchLiveData, (query) -> {
return mSearchRepository.getQuestions(query);
});
SearchViewModel(SearchRepository searchRepository) {
this.mSearchRepository = searchRepository;
}
public LiveData<List<Question>> getQuestionLiveData() {
return mQuestionLiveData;
}
public void setQuery(String query) {
mSearchLiveData.setValue(query);
}
}
SearchRepository:
public class SearchRepository {
//private String inTitle;
private MutableLiveData<List<Question>> mQuestions = new MutableLiveData<>();
public SearchRepository() {
//getQuestionsWithTextInTitle();
}
private void getQuestionsWithTextInTitle(String inTitle) {
ApiService apiService = RestApiClient.getApiService(ApiService.class);
Call<QuestionsResponse> call = apiService.getQuestionsWithTextInTitle(inTitle);
call.enqueue(new Callback<QuestionsResponse>() {
#Override
public void onResponse(Call<QuestionsResponse> call, Response<QuestionsResponse> response) {
QuestionsResponse questionsResponse = response.body();
if (questionsResponse != null) {
mQuestions.postValue(questionsResponse.getItems());
//shouldShowData = true;
} else {
Log.d("SearchRepository", "No matching question");
//shouldShowData = false;
}
}
#Override
public void onFailure(Call<QuestionsResponse> call, Throwable t) {
//shouldShowData = false;
t.printStackTrace();
}
});
}
public LiveData<List<Question>> getQuestions(String inTitle) {
getQuestionsWithTextInTitle(inTitle);
return mQuestions;
}
}
Your approach of passing the search input in through your CustomSearchViewModelFactory and into the constructor for the ViewModel and into the constructor for your SearchRepository isn't going to work in any case. While the first time you search your CustomSearchViewModelFactory creates the ViewModel, the second time you hit search, your SearchViewModel is already created and your factory is not invoked a second time, meaning you never get the second query.
Instead, you should file the ViewModel Overview documentation, and use Transformations.switchMap() to convert your input (the search string) into a new LiveData<List<Question>> for that given query.
This means that your ViewModel would look something like
public class SearchViewModel extends ViewModel {
private SearchRepository mSearchRepository;
private MutableLiveData<String> mSearchLiveData = new MutableLiveData<String>();
private LiveData<List<Question>> mQuestionLiveData =
Transformations.switchMap(mSearchLiveData, (query) -> {
return mSearchRepository.getQuestions(query);
});
public SearchViewModel() {
mSearchRepository = new SearchRepository();
}
public void setQuery(String query) {
mSearchLiveData.setValue(query);
}
public LiveData<List<Question>> getQuestionLiveData() {
return mQuestionLiveData;
}
}
You'd then update your Activity to:
Always observe the getQuestionLiveData() (note that you won't get a callback to your Observer until you actually set the first query)
Call setQuery() on your SearchViewModel in your makeSearch()
Remove your CustomSearchViewModelFactory entirely (it would no longer be needed).

How to change navigation page back button in xamarin forms

I am trying to change back arrow image in navigation page. For this in android app i created navigation page renderer and then using method toolbar.SetNavigationIcon and its not working, but when i use toolbar.SetLogo its working fine.
protected override void OnElementChanged(ElementChangedEventArgs<NavigationPage> e)
{
base.OnElementChanged(e);
toolbar.SetNavigationIcon(Resource.Drawable.arrow);
toolbar.SetLogo(Resource.Drawable.arrow);
}
public override void OnViewAdded(Android.Views.View child)
{
base.OnViewAdded(child);
if (child.GetType() == typeof(Android.Support.V7.Widget.Toolbar))
{
toolbar = (Android.Support.V7.Widget.Toolbar)child;
toolbar.ChildViewAdded += Toolbar_ChildViewAdded;
}
}
I also tried add image to app:navigationIcon in toolbar.axml, and it shows great in designer
my arrow
But, when i starting my app i have the same standart arrow icon
enter image description here
If your MainActivity is FormsApplicationActivity, you could refer to this example :
https://github.com/jessejiang0214/ChangeBackIconInXF/tree/master/Droid
If your MainActivity type is FormsAppCompatActivity, you could custom a PageRenderer and change the Toolbar's NavigationIcon.
For example :
[assembly: ExportRenderer(typeof(ContentPage), typeof(NavigationPageRendererDroid))]
...
public class NavigationPageRendererDroid : PageRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.Page> e)
{
base.OnElementChanged(e);
var context = (Activity)Xamarin.Forms.Forms.Context;
var toolbar = context.FindViewById<Android.Support.V7.Widget.Toolbar>(Droid.Resource.Id.toolbar);
toolbar.NavigationIcon = Android.Support.V4.Content.ContextCompat.GetDrawable(context, Resource.Drawable.Back);
}
}
Usage :
MainPage = new NavigationPage(new MainPage());
...
//When click a button in MainPage, navigate to another page
private async void Button_Clicked(object sender, EventArgs e)
{
await Navigation.PushAsync(new TestPage());
}
Effect.
Update :
When you use Navigation.PushAsync() method navigate to another page, the system will automatically update the Toolbar's icon, you could find in the source code :
protected virtual Task<bool> OnPushAsync(Page view, bool animated)
{
return SwitchContentAsync(view, animated);
}
Task<bool> SwitchContentAsync(Page page, bool animated, bool removed = false, bool popToRoot = false)
{
...
UpdateToolbar();
...
}
void UpdateToolbar()
{
...
bool isNavigated = ((INavigationPageController)Element).StackDepth > 1;
if (isNavigated)
{
...
if (NavigationPage.GetHasBackButton(Element.CurrentPage))
{
//Set the navigation back icon < =================== !!! =-=
var icon = new DrawerArrowDrawable(activity.SupportActionBar.ThemedContext);
icon.Progress = 1;
bar.NavigationIcon = icon;
}
}
...
}
Solution :
So we have no choice but to custom a NavigationPageRenderer, override the OnPushAsync method to set the Toolbar's icon.
using AToolbar = Android.Support.V7.Widget.Toolbar;
[assembly: ExportRenderer(typeof(CustomNavigationPage), typeof(NavigationPageRendererDroid))] // APPCOMP
...
public class NavigationPageRendererDroid : Xamarin.Forms.Platform.Android.AppCompat.NavigationPageRenderer // APPCOMP
{
public AToolbar toolbar;
public Activity context;
protected override Task<bool> OnPushAsync(Page view, bool animated)
{
var retVal = base.OnPushAsync(view, animated);
context = (Activity)Xamarin.Forms.Forms.Context;
toolbar = context.FindViewById<Android.Support.V7.Widget.Toolbar>(Droid.Resource.Id.toolbar);
if (toolbar != null)
{
if (toolbar.NavigationIcon != null)
{
toolbar.NavigationIcon = Android.Support.V4.Content.ContextCompat.GetDrawable(context, Resource.Drawable.Back);
//toolbar.SetNavigationIcon(Resource.Drawable.Back);
}
}
return retVal;
}
}
The CustomNavigationPage are defined in PCL :
public class CustomNavigationPage : NavigationPage
{
public CustomNavigationPage(Page startupPage) : base(startupPage)
{
}
}
Usage :
public App()
{
InitializeComponent();
MainPage = new CustomNavigationPage(new MainPage());
}
...
// In MainPage
private async void Button_Clicked(object sender, EventArgs e)
{
await Navigation.PushAsync(new TestPage());
}
I solved this the next way:
In my MainActivity i am added static toolbar property and identified it in OnCreateOptionsMenu
public static Toolbar ToolBar { get; private set; }
public override bool OnCreateOptionsMenu(IMenu menu)
{
ToolBar = FindViewById<Toolbar>(Resource.Id.toolbar);
ToolBar.SetNavigationIcon(Resource.Drawable.arrow);
return base.OnCreateOptionsMenu(menu);
}
protected override void OnCreate(Bundle bundle)
{
TabLayoutResource = Resource.Layout.Tabbar;
ToolbarResource = Resource.Layout.Toolbar;
....
}
Then in PageRenderer:
protected override void OnElementChanged(ElementChangedEventArgs<Page> e)
{
base.OnElementChanged(e);
MainActivity.ToolBar?.SetNavigationIcon(Resource.Drawable.arrow);
}
But!! From 2 ways i have bad effect with redrawing
Usage:
async void tapImage_Tapped(object sender, EventArgs e)
{
await Navigation.PushAsync(new ChooseGenrePage(_listGenres));
}

Xamarin TimePicker propertychanged event fired on load

I am developing a Xamarin app for android using Xamarin forms to create my layout. I have come across an issue with my time picker firing on load of my view cell.
What I am doing is creating a list view and then setting the item template to my custom view cell template. In my view cell template I am creating a TimePicker and binding a PropertyChanged event to it. I am also setting the TimePicker.Time property with information retrieved from the database. What seems to happen at this point is that my PropertyChanged event is fired for each item that will be displayed in the list view. This leads to multiple database calls that are not needed.
Is there a way to stop the PropertyChanged event being called until a property has actually been changed?
My code is below.
public class MyCell : ViewCell
{
private readonly TimePicker _myTimePicker;
public MyCell()
{
_myTimePicker = new TimePicker()
{
HorizontalOptions = LayoutOptions.EndAndExpand
};
_myTimePicker.SetBinding(TimePicker.TimeProperty, "StartTime");
_myTimePicker.PropertyChanged += MyTimePicker_PropertyChanged;
var viewLayout = new StackLayout()
{
HorizontalOptions = LayoutOptions.StartAndExpand,
Orientation = StackOrientation.Horizontal,
Padding = new Thickness(20),
Children = { _myTimePicker }
};
View = viewLayout;
}
private void MyTimePicker_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == TimePicker.TimeProperty.PropertyName && _myTimePicker.Time != TimeSpan.MinValue)
{
var dataAccess = new DataAccessLayer();
dataAccess.Update(myTimePicker.Time);
}
}
}
I'm not sure why the propertychanged event is fired multiple times and how to stop it firing until I actually pick a time. Any help would be appreciated.
Below is my code for the form to display the list view. All my xaml is defined in code. 'MyCell' uses the values returned from '_dataAccess.GetTimes()'.
public class TimeDetails : ContentPage
{
private ListView _listView;
private readonly DataAccessLayer _dataAccess = new DataAccessLayer();
protected override void OnAppearing()
{
_listView = new ListView
{
RowHeight = 80,
SeparatorColor = Color.Blue,
SeparatorVisibility = SeparatorVisibility.Default
};
_listView.ItemsSource = _dataAccess.GetTimes();
_listView.ItemTemplate = new DataTemplate(typeof(MyCell));
_listView.ItemSelected += ListView_ItemSelected;
Content = new StackLayout
{
VerticalOptions = LayoutOptions.FillAndExpand,
Children = { _listView }
};
}
}
The return type of _dataAcess.GetTimes() is List of TaskTime. The TaskTime model is shown below.
public class TaskTime
{
public int Id { get; set; }
public string Task { get; set; }
public TimeSpan StartTime { get; set; }
}
class MyCell : ViewCell
{
private readonly TimePicker _myTimePicker;
public MyCell()
{
_myTimePicker = new TimePicker()
{
HorizontalOptions = LayoutOptions.EndAndExpand
};
_myTimePicker.SetBinding(TimePicker.TimeProperty, "StartTime");
_myTimePicker.PropertyChanged += MyTimePicker_PropertyChanged;
_myTimePicker.Focused += _myTimePicker_Focused;
var viewLayout = new StackLayout()
{
HorizontalOptions = LayoutOptions.StartAndExpand,
Orientation = StackOrientation.Horizontal,
Padding = new Thickness(20),
Children = { _myTimePicker }
};
View = viewLayout;
}
bool myTimePickerSelected;
private void _myTimePicker_Focused(object sender, FocusEventArgs e)
{
myTimePickerSelected = true;
}
private void MyTimePicker_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == TimePicker.TimeProperty.PropertyName && myTimePickerSelected)
{
//var dataAccess = new DataAccessLayer();
//dataAccess.Update(myTimePicker.Time);
}
}
}
A simpler solution using focused & unfocused event is :
public class CustomTimePicker : TimePicker
{
public event EventHandler TimeChanged;
private TimeSpan StartValue { get; set; }
public CustomTimePicker ()
{
this.Focused += OnFoused;
this.Unfocused += OnUnfocused;
}
private void OnFoused(object sender, FocusEventArgs e)
{
StartValue = this.Time;
}
private void OnUnfocused(object sender, FocusEventArgs e)
{
if (StartValue != this.Time)
{
TimeChanged?.Invoke(this, e);
}
}
}
what i have seen is that OnAppearing() is finished when all initialization is done. Maybe you could introduce a boolean variable that you then use in PropertyChanged() method. It is a trick because you have to do it from ContentPage, and pass a value to your control when initialization is finished.
I just saw, ther is OnAppearing() method in CellView, this way everything stays in your control class.

How can i pass an IDialogInterfaceOnDismissListener without my application crashing?

I've got an issue with my Xamarin.Android application regarding IDialogInterfaceOnDismissListener.
My repro sample:
public enum ConfirmationResult
{
Cancel,
Yes,
No
}
public interface IDialogService
{
Task<ConfirmationResult> ConfirmAsync(string title, string message, string acceptText, string declineText);
}
public class DialogService : IDialogService
{
class DismissDelegateListener : IDialogInterfaceOnDismissListener, IDialogInterfaceOnCancelListener
{
private Action _callback;
public DismissDelegateListener()
{
}
public DismissDelegateListener(Action callback)
{
_callback = callback;
}
public void Dispose()
{
_callback = null;
}
public IntPtr Handle { get; }
public void OnCancel(IDialogInterface dialog)
{
_callback?.Invoke();
}
public void OnDismiss(IDialogInterface dialog)
{
_callback?.Invoke();
}
}
private readonly WeakReference<Context> _activity;
public DialogService(Context activity)
{
_activity = new WeakReference<Context>(activity);
}
public async Task<ConfirmationResult> ConfirmAsync(string title, string message, string acceptText, string declineText)
{
Context target;
if (!_activity.TryGetTarget(out target))
return ConfirmationResult.Cancel;
TaskCompletionSource<ConfirmationResult> tcs = new TaskCompletionSource<ConfirmationResult>();
var builder = new AlertDialog.Builder(target);
var d = builder
.SetTitle(title)
.SetMessage(message)
.SetPositiveButton(acceptText, (sender, args) =>
{
tcs.SetResult(ConfirmationResult.Yes);
(sender as AlertDialog)?.Dispose();
})
.SetNegativeButton(declineText, (sender, args) =>
{
tcs.SetResult(ConfirmationResult.No);
(sender as AlertDialog)?.Dispose();
})
// if this line is uncommented my application crashes once the dialog is opened
// .SetOnCancelListener(new DismissDelegateListener(() => { tcs.SetResult(ConfirmationResult.Cancel); }))
.Create();
d.Show();
return await tcs.Task;
}
}
As soon as i call SetOnCancelListener my application will crash with the error message System.InvalidCastException: Specified cast is not valid.
To me everything appears to be correct - i am passing an implementation of the requested interface and my implementation isn't all that difficult either.
Does this problem ring a bell to anyone?
Deriving from Java.Lang.Object was the solution.
class DismissDelegateListener : Java.Lang.Object, IDialogInterfaceOnDismissListener, IDialogInterfaceOnCancelListener
//class DismissDelegateListener : IDialogInterfaceOnDismissListener, IDialogInterfaceOnCancelListener
{
private Action _callback;
public DismissDelegateListener()
{
}
public DismissDelegateListener(Action callback)
{
_callback = callback;
}
protected override void Dispose(bool disposing)
{
_callback = null;
base.Dispose(disposing);
}
public void OnCancel(IDialogInterface dialog)
{
_callback?.Invoke();
}
public void OnDismiss(IDialogInterface dialog)
{
_callback?.Invoke();
}
}

MVVMCross: Binding Two Way doesn't update viewmodel

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.

Categories

Resources