How to get ContentPage's BindingContext from ContentView? - xamarin

I have the following Contentpage.content, where I set certain binding context.
<StackLayout>
<local:Post />
<local:Comments />
</StackLayout>
In Post.xaml.cs (ContentView), I've tried to get the binding context of the ContentPage this way but it doesn't work.
BindingContext = (PostViewModel)Parent.BindingContext;
How can I get the binding context of the ContentPage if I'm standing in a ContentView?

By the time your constructor is called, the BindingContext might not be initialised yet.
So, you should wait for the BindingContext being changed to perform operations on it.
I think the answer is OnBindingContextChanged event.
https://developer.xamarin.com/api/member/Xamarin.Forms.View.OnBindingContextChanged()
Little sample:
protected override void OnBindingContextChanged ()
{
base.OnBindingContextChanged ();
//BindingContext should not be null at this point
// and you may add your code here.
}
Note:
If you have a ContentView inside a ContentPage, unless explicitly set by another Control (like when using an ItemTemplate for a ListView) or by your code, the BindingContext of the ContentView is the same as the ContentPage.
So, it shouldn't be necessary to call "Parent".
Let me know if more clarification is needed.

The BindingContext of the ContentView is usually also the BindingContext of the ContentPage since it is passed down from the parent.
So you should not even need to set ContentView.BindingContext if you already set the parent ContentPage.BindingContext.
If I am missing something, please let me know.

Related

Xamarin Forms - Adding constructor to MasterDetailPage.Master

I have a Xamarin.Forms app with a master-detail page and it works well.
But I've recently needed to add a parameter to the constructor of the master page (AttendPageMaster), but now I need to pass this constructor.
How do I add a parameter to the xaml?
<MasterDetailPage.Master>
<pages:AttendPageMaster x:Name="MasterPage" />
</MasterDetailPage.Master>
The code behind page with constructor:
public AttendPageMaster(AttendanceViewModel viewModel)
{
}
Let me know if you need any more info.
You do not have to pass ViewModel to Page via constructor, you can set the Page's BindingContext:
<MasterDetailPage.Master>
<pages:AttendPageMaster x:Name="MasterPage">
<pages:AttendPageMaster.BindingContext>
<myViewModels:AttendanceViewModel />
</pages:AttendPageMaster.BindingContext>
</pages:AttendPageMaster>
</MasterDetailPage.Master>
This solution will work if your ViewModel does not expect any parameters in constructor. Otherwise you may consider using ViewModelLocator and DI to inject the constructor parameters.
Please note that myViewModels should be defined in the header of your XAML page as xmlns:myViewModels.
P.S.: Previously you mentioned that you got an exception while trying to use code behind approach. You could easily solve it by setting the Title property of the AttendPageMaster. Example:
new AttendPageMaster(new AttendanceViewModel()){ Title = " " };
I managed to do this from the code behind by creating the menu page in the constructor of the masterdetail and assigning it to the "Master" property:
AttendMasterPageMaster MasterPage;
public AttendMasterPage(AttendanceViewModel viewModel)
{
MasterPage = new AttendMasterPageMaster(viewModel);
Detail = new NavigationPage((Page)Activator.CreateInstance(typeof(StartPage), viewModel));
Master = MasterPage;

FFImageLoading : load into an ImageButton

In Xamarin.Android, to load an image using FFImageLoading, a ImageViewAsync must be used instead of a standard ImageView.
I couldn't find what I can use if I wanna load my image into an ImageButton. Didn't find an "ImageButtonAsync".
Dont know if this is relevant or not. I gave my svg a tap gesture to get it working like a button.
first reference the FFImageLoading.Svg.Forms in your xaml
namespace:FFImageLoading.Svg.Forms;assembly=FFImageLoading.Svg.Forms"
second add the svgImage with a tap gesture
<ffimageloadingsvg:SvgCachedImage Source="YourImageSource" >
<ffimageloadingsvg:SvgCachedImage.GestureRecognizers>
<TapGestureRecognizer NumberOfTapsRequired="1" Command="{Binding YourCommand}"/>
</ffimageloadingsvg:SvgCachedImage.GestureRecognizers>
</ffimageloadingsvg:SvgCachedImage>
You will call this in the constructor of your ViewModel to initialize the command
YourCommand= new DelegateCommand(async () => await ExecuteYourCommandAsync());
Create the properties that bind to the xaml file
public ICommand YourCommand
{
get => yourCommand;
set => SetProperty(ref yourCommand, value);
}
private ICommand yourCommand;
The await ExecuteYourCommandAsync is a method that you create. and in there you will put your logic of what you actually want the tap gesture to do.
You can also pass through object with the command. let met know if this makes sense
AFAIK there is no particular ImageButton to use with FFImageLoading but you can use directly an ImageViewAsync set it as android:clickable="true" and add the click event.
And as stated here an ImageButton is just an ImageView that has a non-null background by default and also, ImageButton.onSetAlpha() method always returns false, scaleType is set to center and it's always inflated as focusable. So if you need that behaviour you can add it.
Another way would be you to create your custom ImageButton that can handle FFImageLoading image loading. For that you can inherit from ImageViewAsync and add the behaviours explained in the paragraph before. So that you can use the FFImageLoading API directly as this custom controls inherits ImageViewAsync.
Instead of that you can also add your custom loading logic as explained here inheriting from ImageLoaderTask and call it like ImageService.Instance.LoadImage(customImageTask)
Finally another way (but hackish and non-performant) would be to create an ImageViewAsync just to hold the result of the FFImageLoading and on the Success callback set the Drawable in the ImageButton:
var myImageButton = myView.FindViewById<ImageButton>(Resource.Id.my_image_button);
var myImageView = new ImageViewAsync(this); // this is the context
ImageService.Instance.LoadUrl(this.Source[position].LogoImagePath)
.Success(() => myImageButton.SetImageDrawable(myImageView.Drawable))
.Into(myImageView);
HIH and if you need any help let me know

ToolbarItem isEnabled property is available in xaml, not code

I have a form where a user can enter some data and save it to a database. I have been trying to add basic validation, making a user have to enter a value in every field. I want my save button to be unavailable until the user has entered some information in every form.
Here is my button:
<ToolbarItem Name="MenuItem1" Order="Primary" Text="Save" Priority="1" Command="{Binding SaveDataCommand}" IsEnabled="{Binding CanSaveData}"/>
I can access the isEnabled property in the XAML but where this gets infuriating is that I can't then reupdate this property. The button gets stuck in whatever state I tell it on load. I have checked my view model and it is returning a boolean which is correct (printing it to the console), it's just there is no way of updating the button state.
I even tried adding a new button that would force update the enabled state however this wouldn't work
<ToolbarItem x:Name="whyxamarinwhy" Name="MenuItem1" Order="Primary" Text="Save" Priority="1" Command="{Binding SaveEvent}" IsEnabled="{Binding CanCreateEvent}"/>
whyxamarinwhy.isenabled = true; //I cannot access this property because it doesn't exist.
Is the only way to implement this functionality going to be using a custom renderer?
MenuItem.IsEnabledProperty / For internal use by the Xamarin.Forms platform.
You should use the Command's CanExecute of the ToolbarItem to determine if the Execute method can be triggered.
Since you are already binding the command to SaveDataCommand, you can toggle the return of the CanExecute of that command to determine if the menu item button should be allowed to trigger the attached command.
You did not post your ViewModel, but in a inner-class ICommand implementation, something as simple as this works:
static bool SaveDataCommandCanExecute = true;
class SaveDataCommand : ICommand
{
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
// your code needs to toggle SaveDataCommandCanExecute to determine if the Execute method can be triggered
return SaveDataCommandCanExecute;
}
public void Execute(object parameter)
{
// Do something
}
}

Xamarin XAML variable scope not working like expected

I have a really odd problem with variable scopes. A Listview named "TodoListView" is defined via xaml, and it's ItemSource populated from a SQListe database. Works. Inside the ListView I have a ViewCell to display the data row-wise.
<ContentPage ... x:Class="JanitorPro.MainPage" ...>
<StackLayout>
<ListView x:Name="TodoListView" Margin="20" ItemSelected="OnListItemSelected">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Horizontal" HorizontalOptions="FillAndExpand">
<Label Text="{Binding name}" VerticalTextAlignment="Center" HorizontalOptions="StartAndExpand" />
<Switch HorizontalOptions="End" IsToggled="{Binding done}" Toggled="DoneSwitchToggled"/>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
The codebehind looks like this (some irrelevant portions removed):
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
}
protected override async void OnAppearing()
{
base.OnAppearing();
// load Database
TodoListView.ItemsSource = await App.TodoDatabase.GetItemsAsync("SELECT * FROM [TodoItem]");
}
async void OnReloadButtonClicked(object sender, EventArgs e)
{
Debug.WriteLine("Reload Button Click");
TodoListView.ItemsSource = await App.TodoDatabase.GetItemsAsync("SELECT * FROM [TodoItem]");
Debug.WriteLine("Reload done.");
}
async void OnListItemSelected(object sender, SelectedItemChangedEventArgs e)
{
if (e.SelectedItem != null)
{
await Navigation.PushAsync(new TodoItemPage
{
BindingContext = e.SelectedItem as TodoItem
});
}
}
private void DoneSwitchToggled(object sender, ToggledEventArgs e)
{
// TodoItem i = null;
TodoItem i = TodoListView.SelectedItem;
if (i != null)
{
Debug.WriteLine("Toggle: {0}", i.id);
}
}
}
}
The oddity has two stages. Before I inserted the DoneSwitchToggled event handler, every occurrance of TodoListView.ItemsSource got a red underline under TodoListView and a hint that "The name TodoListView does not exist in the current context". OK, I thought that VS was not smart enough to find a definition in the xaml file, because, despite of the warning, the program compiled and ran fine. TodoListView gets initialized and does correctly display the rows of the underlying database, so it does clearly exist at runtime.
Things went wild when I added the DoneSwitchToggled event handler to both XAML and the codebehind. All the sudden the program won't compile any longer but bail out with a CS0103 error "The name "TodoListView" does not exist in the current context". The error appears three times, with the line numbers pointing to the other occurrances of TodoListView in onAppearing() and OnReloadButtonClicked(). Huh? How the heck can the addition of a variable reference in an event handler render that variable invalid in completely different methods? OK, there was something fishy with the variable before (still don't know what ...), but it worked. Now it doesn't any more, whch doesn't make any sense for me. Furthermore, if I comment out the offending line in the DoneSwitchToggled event handler, and insert a dummy definition for i, like so:
TodoItem i = null;
// TodoItem i = TodoListView.SelectedItem;
everything is like before, VS still underlines the other appearances of TodoListView, but now the program builds and runs ok again.
Anyone who can explain this effect, and show me how correct my code? I think the objective is clear: DoneSwitchToggled is supposed to write back the switch value into the database (and do some other processing not shown in my stripped down sample), and though the "sender" object is correctly set to reference my button, I found no way to access the underlying data binding, since ToggledEventArgs unfortunately does seem to only pass the switch position "true" or "false", but - unlike the OnListItemSelected event handler - not pass any reference to the bound row through the second argument. So my idea was to use ListView.SelectedItem for this purpose.
Finally I figured it out myself. This seems to be a glitch in VS 2017. There is nothing wrong with TodoListView, so error CS0103 is misleading nonsense.
What VS really means is an error CS0266. TodoListView is defined by a generic list
List<TodoItem>
to access SelectedItem i need to typecast it:
TodoItem i = (TodoItem)TodoListView.SelectedItem;
This added, all errors are gone, app code builds OK.
Btw, unfortunately this approach to get at the item where the Switch has been flipped has proven not working, TodoListView does always return null for SelectedItem, seems that the ListView control doesn't see the button press. Need to find a different way to find the list item beneath the switch to get at the bound row id.

Xamarin Forms Force view to bind values

I have a view. I have a bindable property there.
public partial class OrderCard : ContentView
{
public static readonly BindableProperty OrderProperty = BindableProperty.Create(nameof(Order), typeof(Order), typeof(OrderCard), null);
public Order Order
{
get { return (Order)GetValue(OrderProperty); }
set { SetValue(OrderProperty, value); }
}
public OrderCard()
{
InitializeComponent();
}
}
In the xaml of this view I'm binding to Order property like this:
Text="{Binding Order.Name, Source={x:Reference Root}}"
Root is a name in the xaml of a view OrderCard
When I use this view in the page everything works ok.
But I want to measure it's size even before adding it to the page.
var orderCard = new OrderCard { Order = order};
SizeRequest sizeRequest = orderCard.Measure(OrdersContainer.Width/5, OrdersContainer.Height);
But it gives me wrong numbers because bindings isn't applied.
How to force to apply bindings when view isn't attached to the page?
Bindings do not require being attached to a Page or anything else to be applied.
You might think they're not applied if your method for figuring out is to set a breakpoint on get_Order because that is never used, the Xaml loader uses GetValue directly. The usual way of figuring out if a Binding is correctly applied, is to add a PassthroughConverter (don't look for it, you have to write it yourself) to the Binding and put the breakpoint in the Convert method.
That being said, you can't Measure anything unless it's added to a page that is rendered on screen. If you try to Measure before that, you indeed get dummy values.
I was able to solve this problem by not doing a property Order but passing an order as BindingContext. Then I can measure the size of a view without attaching it to a page like this:
var orderCard = new OrderCard { BindingContext = order};
SizeRequest sizeRequest = orderCard.Measure(widthToTryToFitInTheView,heightToTryToFitInTheView);

Resources