Displaying an ItemDetailsPage for a ListView item – Part 3 – Writing a Xamarin.Forms App

My adventures in “I can has MVVM” continues! Please see Part 1 (what this app is) and Part 2 (ListView).

If I’ve misunderstood something, please let me know! The only way to combat Impostor Syndrome is to fight it head on.

To follow along, go to my ListViewInTabbedControl and  `git checkout tags/ItemDetailsPage` or view creating the ItemDetailsPage here and view the adding the commanding diff here.

What I learned

  • There’s an excellent pluralsight course on async and await called Getting Started with Async Programming in .NET
  • Difference between PushAsync and PushModalAsync
  • Why do someFabric as Fabric instead of (Fabric) someItem as seen in this diff? If you use (Fabric), it’ll throw an exception immediately if cast fails, but if you do “as fabric”, it’ll just set to null so you can do things with it. You could always do a try catch if you use (Fabric), but it’s less code to write / cleaner

If you can eventually debug your issue, debugging is a great teacher! 🙂

While writing this post, I found a bug from Blog Post 1.

Originally I was going to refer to a scene from 13 Assassins (2011). Wait, hear me out 🙂 I was thinking of that scene towards the end of the movie where the guy gets hit and experiences pain for the first time, and the other guy says something like “well, that was unexpected.” For some reason, debugging this issue made me think of that scene /shrugs 🙂

When I tried to open the ItemDetailsPage, I got

NotUsingNavigationPage.png

Say the classic words with me, “But how is this working on my other app?” I know this code below should work to show the I​temDetailsPage

await this.Navigation.PushAsync(new ItemDetailsPage(tappedFabric));

So I tried to give it a NavigationPage…

await this.Navigation.PushAsync(new NavigationPage(new ItemDetailsPage(tappedFabric)))

and I got the same PushAsync is not supported globally on iOS, please use a NavigationPage exception. Grr.

Finally I realized which “Page” the exception was referring to. It’s not referring to the page I was trying to pass it. It’s referring to the page that’s the caller for this.Navigation.PushAsync 

What I mean is in App.xaml.cs, although I created navigation pages for these pages,

var page1Nav = new NavigationPage(page1);
var page2Nav = new NavigationPage(page2);

I neglected to add them to the tabbed control. Notice below I added page1 and page2 and not page1Nav and page2Nav, hence the exception.

var tabbed = new TabbedPage();
tabbed.Children.Add(page1);  // whoops! not a navigation page
tabbed.Children.Add(page2);  // whoops! not a navigation page

Make sure to change these lines to

tabbed.Children.Add(page1Nav);
tabbed.Children.Add(page2Nav);

Okay now back to your originally scheduled blog post, already in progress…

1) User taps an item to be edited in its own page

We’ll use `ItemTapped=”Handle_ItemTapped”` here in the code behind, since there’s not a corresponding bindable property, as you see in SelectedItem.

But what does “not a bindable property” mean? You might be tempted to try “ItemTapped="{Binding ItemTapped}" but that won’t work. You’ll get

Specific Cast Not Valid complier error message

You’ll need to use a click event handler in the code behind for item tapped. Why not just use ItemSelected? Well, the issue is when the user wants to edit the same item twice in a row. For example, say the user edits Fabric 1. Upon returning, this item will still be selected. Although it’s in an “inactive selection” state, you can’t select it again, meaning the user will try to tap it, but nothing will happen. The app will still just look at you. Hence for this UX design requirement, you need to use ItemTapped. And IMO, “tapped” is more descriptive of the action the user is performing than “selecting”.

Add the following code to Page1.xaml

ItemTapped="{Binding ItemTapped}

The ListView now gets its content from the ObservableCollection of Fabrics. I called this OCFabrics to break my reliance on it for all the app’s needs.

And in the codebehind for Page1.xaml.cs, add the following:

async void Handle_ItemTapped(object sender, Xamarin.Forms.ItemTappedEventArgs e){
   Fabric tappedFabric = e.Item as Fabric;

   if (tappedFabric == null)
       return;

   await this.Navigation.PushModalAsync(new ItemDetailsPage(tappedFabric));

I’ll go over the significance of PushAsync vs PushModalAsync at the bottom of this post.

2) Create the ItemDetailsPage() – what the user will see when he/she taps on an item

Add a new View called `ItemDetailsPage` under Views. (This is a Forms – ContentPage Xaml in VS for Mac). And a code behind called ItemDetailsViewModel.

Use the following code for the ItemDetailsPage

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="ListViewInTabbedControl.Views.ItemDetailsPage"
Title="{Binding Title}">
   <ContentPage.Content>
        <StackLayout>
            <Label Text="{Binding Title}"
VerticalOptions="CenterAndExpand"
HorizontalOptions="CenterAndExpand" />
        </StackLayout>
    </ContentPage.Content>

and for the codebehind

public ItemDetailsPage(Fabric fabric) {
  InitializeComponent();

  var itemDetailsViewModel = new ItemDetailsViewModel(this.Navigation, fabric);
  this.BindingContext = itemDetailsViewModel;
}

Run this and now you’ll get

displaying Fabric1 when tapped in its own page

awesome!

3) Now we need a way to escape by adding a Done button and a DoneEditing command.

In the ItemsDetailsViewModel, add a new property for the command

 public ICommand DoneEditingCommand { get; private set; }

in the ItemsDetailsViewModel constructor, create the new command, as shown below

public ItemDetailsViewModel(INavigation Navigation, Fabric fabric) {
            Title = "Hello from Item " + fabric.Name;
            this.Navigation = Navigation;
            DoneEditingCommand = new Command(DoneEditing);
}

where the `DoneEditing` is the delegate. Add the DoneEditing method in the ItemsDetailsViewModel class, e.g.

        private async void DoneEditing() {
            await Navigation.PopModalAsync();
        }

in the ItemDetailsPage, add a button to your StackLayout

<Button x:Name="DoneButton" Text="Done" FontSize="Large"
Command="{Binding DoneEditingCommand}"
HorizontalOptions="Center" VerticalOptions="End"/>

and voila! You can now hit done and skip around! The item tapped will be shown in the Details Page, e.g. Fabric3 will say “Hello from Fabric 3” with a Done button at the bottom.

Fabric 3 displayed in its ItemDetailPage

Why PushAsync instead of PushModalAsync?

It depends what you’re trying to achieve UX-wise. Do you want the ItemDetailsPage to slide in from the bottom or from the right? Do you want a “back” button in the header on iOS? Do you want a title?

Not sure? Let’s experiment!

The code above is sliding in from the bottom as a Modal pop-up. For sliding in from the right with a back button and a title, you’ll just do

In Handle_ItemTapped() do

await this.Navigation.PushAsync(new ItemDetailsPage(tappedFabric));

and have DoneEditing() to a corresponding

await Navigation.PopAsync();

both the `< Page 1` back button and the Done slide back.

HelloFromFabric1NonModal.jpg

MO this feels like a better UX.

BTW, that back button comes from using NavigationPages as the children of the Tabbed control, as mentioned at the top of my post

But let’s try out the PushModalAsync() to be certain which UX experience we want. Change both Handle_ItemTapped() and DoneEditing() to use Modal. Remember: if you push Modal you need to Pop Modal.

Notice how the Page appears from the bottom, but also no title or back button.

ItemDetailNoBackButton.jpg

next blog post will talk about editing the items.

Leave a comment