자알못 자마린: Xamarin.Forms ListView 기초

 

ListView

ListView는 Item을 담아 목록을 보여주는 View다.

주요 속성

  • SeparatorVisibility: 구분선 표시 여부
  • SeperatorColor: 구분선 컬러

XAML

<ListView x:Name="listView" SeparatorVisibility="None" SeparatorColor="Black"></ListView>

C#

using System.Collections.Generic;
using Xamarin.Forms;

namespace MyFirstXamarinApp
{
    public partial class ListPage : ContentPage
    {
        public ListPage()
        {
            InitializeComponent();

            var list = new List<string>
            {
                "라이언", "어피치", "프로도"
            };

            listView.ItemsSource = list;
        }
    }
}

좌: SeparatorVisibility=None, 우: SeparatorVisibility=Default

 

ListView Template 사용하기

이전 예제는 문자열만을 List<T>에 등록하여 보여주는 예제다. ListView가 기본적으로 제공하는 템플릿을 사용하면 간단히 텍스트 내용, 상세 내용, 이미지 등을 보여줄 수 있다.

ListView Item 클래스 생성

ListView에 어떤 내용(Item)이 들어갈 지를 정의할 클래스를 만든다.

여기서는 간단한 프로필 정보(이름, 상태메시지, 프로필사진)를 정의하는 Contact 클래스를 만든다.

namespace MyFirstXamarinApp.Models
{
    public class Contact
    {
        public string Name { get; set; }
        public string Status { get; set; }
        public string ImageUri { get; set; }
    }
}

ListView에서 제공하는 템플릿 사용: TextCell, ImageCell

ListView에 대한 속성 요소 구문을 사용하여 ListView의 내용을 어떻게 보여줄 것인지를 정의한다.

아래는 텍스트만을 사용하는 TextCell을 사용한 ListView 예제.

<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="MyFirstXamarinApp.ListPage">
    <ListView x:Name="listView">
        <ListView.ItemTemplate>
            <DataTemplate>
                <TextCell Text="{Binding Name}" Detail="{Binding Status}"></TextCell>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</ContentPage>

이미지도 보여주려면 ImageCell을 사용한다.

<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="MyFirstXamarinApp.ListPage">
    <ListView x:Name="listView">
        <ListView.ItemTemplate>
            <DataTemplate>
                <ImageCell Text="{Binding Name}" Detail="{Binding Status}" ImageSource="{Binding ImageUri}"></ImageCell>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</ContentPage>

ListView에 어떤 Item을 포함할지 정의

using System.Collections.Generic;
using MyFirstXamarinApp.Models;
using Xamarin.Forms;

namespace MyFirstXamarinApp
{
    public partial class ListPage : ContentPage
    {
        public ListPage()
        {
            InitializeComponent();

            listView.ItemsSource = new List<Contact>{
                new Contact{Name="라이언", Status="안녕요?ㅎ", ImageUri="http://lorempixel.com/100/100/people/1"},
                new Contact{Name="어피치", Status="하이요?ㅎ", ImageUri="http://lorempixel.com/100/100/people/2"},
                new Contact{Name="프로도", Status="헬로우?ㅎ", ImageUri="http://lorempixel.com/100/100/people/3"}
            };
        }
    }
}

실행 화면

좌: TextCell 사용, 우: ImageCell 사용

 

ListView Item Custom: ViewCell

이전에 실습한 ListView의 Item 오른쪽에 Follow 버튼을 추가한다고 해보자. 이 경우 기본적으로 제공되는 TextCell 및 ImageCell로 구현하기에 한계가 있다.

ViewCell을 통해 Item을 어떻게 보여줄지 세부적으로 디자인할 수 있다.

<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="MyFirstXamarinApp.ListPage">
    
    <!-- HasUnevenRows: List Cell의 열 크기를 균등하게 할 것인지를 정의 -->
    <!-- 이 값이 false면 열의 크기가 충분하지 않을 때 레이아웃이 틀어질 수 있음 -->
    <ListView x:Name="listView" HasUnevenRows="true">
        <ListView.ItemTemplate>
            <DataTemplate>
                
                <!-- ViewCell 안에 여러가지 View를 정의 --> 
                <ViewCell>
                    <StackLayout Orientation="Horizontal" Padding="5">
                        <Image Source="{Binding ImageUri}"></Image>
                        <StackLayout Orientation="Vertical" HorizontalOptions="StartAndExpand">
                            <Label Text="{Binding Name}"></Label>
                            <Label Text="{Binding Status}" TextColor="Gray"></Label>
                        </StackLayout>
                        <Button x:Name="btnFollow" Text="Follow"></Button>
                    </StackLayout>
                </ViewCell>
                
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
    
</ContentPage>

 

ListView를 Group별로 보여주기

ListView Group 정의 클래스 생성

ListView에 들어갈 Group에 어떤 정보를 담을지 나타내는 클래스를 만든다.

여기서는 제목과 소제목을 나타내는 ContactGroup 클래스를 만든다.

namespace MyFirstXamarinApp.Models
{
    /* List Group을 정의하는 클래스는 어떤 List를 상속받을지 결정해야 함 */
    public class ContactGroup : List<Contact>
    {
        public string Title { get; set; }
        public string ShortTitle { get; set; }

        public ContactGroup(string title, string shortTitle)
        {
            Title = title;
            ShortTitle = shortTitle;
        }
    }
}

ListPage.xaml

이전에 실습한 ListView Item Custom의 XAML 파일 내용을 그대로 사용한다.

ListView의 Group 관련 속성

  • IsGroupEnabled: 그룹 사용 여부
  • GroupDisplayBinding: 그룹 제목에 보여줄 값
  • GroupShortNameBinding: 그룹 소제목에 보여줄 값
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="MyFirstXamarinApp.ListPage">
    
    <ListView x:Name="listView" HasUnevenRows="true" 
              IsGroupingEnabled="true" GroupDisplayBinding="{Binding Title}" GroupShortNameBinding="ShortTitle">
        <ListView.ItemTemplate>
            <DataTemplate>
                
                <!-- ViewCell 안에 여러가지 View를 정의 --> 
                <ViewCell>
                    <StackLayout Orientation="Horizontal" Padding="5">
                        <Image Source="{Binding ImageUri}"></Image>
                        <StackLayout Orientation="Vertical" HorizontalOptions="StartAndExpand">
                            <Label Text="{Binding Name}"></Label>
                            <Label Text="{Binding Status}" TextColor="Gray"></Label>
                        </StackLayout>
                        <Button x:Name="btnFollow" Text="Follow"></Button>
                    </StackLayout>
                </ViewCell>
                
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</ContentPage>

ListPage.xaml.cs

각 ListView Group에 어떤 Item을 포함할지를 정의한다.

using System.Collections.Generic;
using MyFirstXamarinApp.Models;
using Xamarin.Forms;

namespace MyFirstXamarinApp
{
    public partial class ListPage : ContentPage
    {
        public ListPage()
        {
            InitializeComponent();

            listView.ItemsSource = new List<ContactGroup>{
                new ContactGroup("카카오 프렌즈", "Kakao Friends"){
                    new Contact{Name="라이언", Status="안녕요?ㅎ", ImageUri="http://lorempixel.com/100/100/people/1"},
                    new Contact{Name="어피치", Status="하이요?ㅎ", ImageUri="http://lorempixel.com/100/100/people/2"},
                    new Contact{Name="프로도", Status="헬로우?ㅎ", ImageUri="http://lorempixel.com/100/100/people/3"}
                },

                new ContactGroup("라인 프렌즈", "Line Friends"){
                    new Contact{Name="브라운", Status="안녕요?ㅎ", ImageUri="http://lorempixel.com/100/100/people/4"},
                    new Contact{Name="제임스", Status="하이요?ㅎ", ImageUri="http://lorempixel.com/100/100/people/5"},
                    new Contact{Name="코니", Status="헬로우?ㅎ", ImageUri="http://lorempixel.com/100/100/people/6"}
                }
            };
        }
    }
}

실행 화면

 

ListView 이벤트 처리

ListView의 주요 이벤트

  • ItemTapped: Item을 터치(탭)했을 때 발생하는 이벤트
  • ItemSelected: Item을 선택했을 때 발생하는 이벤트

ListPage.xaml

<ListView x:Name="listView" ItemTapped="Handle_ItemTapped" ItemSelected="Handle_ItemSelected"></ListView>

ListPage.xaml.cs

아이템을 처음 터치하면 ItemTapped 및 ItemSelected 이벤트 2개가 발생한다. 선택된 아이템을 다시 터치하면 ItemTapped 이벤트만 발생한다.

namespace MyFirstXamarinApp
{
    public partial class ListPage : ContentPage
    {
        public ListPage()
        {
            InitializeComponent();

            // 생략...
        }

        /* e: 이벤트 정보를 담고 있는 매개변수 */
        void Handle_ItemSelected(object sender, Xamarin.Forms.SelectedItemChangedEventArgs e)
        {
            // e.SelectedItem: object 타입. 선택된 List Item의 타입(여기서는 Contact 타입)
            var contact = e.SelectedItem as Contact;
            DisplayAlert("Selected", contact.Name, "OK");

            /* List Item이 선택되지 않도록 하려면 List 객체의 SelectedItem 객체를 null로 지정 */
            //listView.SelectedItem = null;
        }

        void Handle_ItemTapped(object sender, Xamarin.Forms.ItemTappedEventArgs e)
        {
            // e.Item: object 타입. 탭한 List Item의 타입(여기서는 Contact 타입)
            var contact = e.Item as Contact;
            DisplayAlert("Tapped", contact.Name, "OK");
        }
    }
}

실행 결과

 

ListView의 Context Action

Context Action은 ListView의 Item을 좌우로 스와이프 할 때 버튼이 나타나는 행동이다.

ListPage.xaml

ContextAction의 CommandParameter 속성을 사용하여 MenuItem에 대응하는 ListView의 Item을 사용할 수 있도록 한다.

<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="MyFirstXamarinApp.ListPage">
    <ListView x:Name="listView">
        <ListView.ItemTemplate>
            <DataTemplate>
                <TextCell Text="{Binding Name}" Detail="{Binding Status}">
                    <TextCell.ContextActions>
                        <!-- CommandParameter는 MenuItem과 이벤트 처리기의 연결을 위한 속성 -->
                        <!-- MenuItem에 대응하는 ListView Item의 모든 속성을 가져오기 위해 바인딩 값을 .으로 지정 -->
                        <MenuItem Text="Call" Clicked="Call_Clicked" CommandParameter="{Binding .}"></MenuItem>
                        
                        <!-- isDestruction 속성: Context Action을 빨간색으로 표시 여부 설정 -->
                        <MenuItem Text="Delete" Clicked="Delete_Clicked" IsDestructive="true" CommandParameter="{Binding .}"></MenuItem>
                    </TextCell.ContextActions>
                </TextCell>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</ContentPage>

ListPage.xaml.cs

ListView에 List<T> 대신 ObservableCollection<T>를 사용하면 Item을 추가하거나 제거해도 ListView가 자동 갱신된다.

MenuItem Click 이벤트 객체는 ListView의 Item에 대한 정보를 갖고있지 않다. 이를 사용하려면 이벤트 발생 객체의 정보가 담긴 sender 객체를 사용해야 한다.

  • object sender -> MenuItem.CommandParameter -> ListView Item
using System.Collections.Generic;
using System.Collections.ObjectModel; // ObservableCollection
using MyFirstXamarinApp.Models;
using Xamarin.Forms;

namespace MyFirstXamarinApp
{
    public partial class ListPage : ContentPage
    {
        // ListView에 List<T>를 사용하면 List<T>의 내용이 바뀌어도 ListView가 이를 인지하지 못함
        // List<T> 대신 ObservableCollection<T>을 사용하면 내용이 바뀔 때 이벤트가 발생함

        // Xamarin.Forms의 ListView는 ObservableCollection의 작동을 인지하여 ListView를 자동 갱신함
        // 따라서 ListView의 내용을 갱신하기 위한 코드를 작성할 필요가 없음
        private ObservableCollection<Contact> _contacts;

        public ListPage()
        {
            InitializeComponent();

            _contacts = new ObservableCollection<Contact>{
                new Contact{Name="라이언", Status="안녕요?ㅎ", ImageUri="http://lorempixel.com/100/100/people/1"},
                new Contact{Name="어피치", Status="하이요?ㅎ", ImageUri="http://lorempixel.com/100/100/people/2"},
                new Contact{Name="프로도", Status="헬로우?ㅎ", ImageUri="http://lorempixel.com/100/100/people/3"}
            };

            // ListView의 ItemSource에 ObservableCollection<T> 객체 사용
            listView.ItemsSource = _contacts;
        }

        void Call_Clicked(object sender, System.EventArgs e)
        {
            // MenuItem.Click 이벤트 객체는 ListView의 Item에 대한 정보를 갖고있지 않음
            // sender 객체를 통해 object 타입 형태로 전달받은 후 MenuItem 타입으로 캐스팅하면 ListView의 Item 정보를 사용할 수 있음
            var menuItem = sender as MenuItem;

            // MenuItem.CommandParameter 속성을 사용하여 ListView Item 객체를 가져올 수 있음
            // 정리: object sender -> MenuItem menuItem.CommandParameter -> Contact contact
            var contact = menuItem.CommandParameter as Contact;

            DisplayAlert("Call", contact.Name, "OK");
        }

        void Delete_Clicked(object sender, System.EventArgs e)
        {
            var contact = (sender as MenuItem).CommandParameter as Contact;

            // ListView.ItemSource 속성은 IEnumerable 타입
            // 순차 접근 방식이므로 직접 원하는 위치에 요소를 추가하거나 제거할 수 없음
            // ObservableCollection<T>를 사용하여 요소를 추가하거나 제거해야 함
            _contacts.Remove(contact);
        }
    }
}

실행 화면

 

ListView를 아래로 당겨 갱신하기

Pull To Refresh를 위한 ListView의 주요 속성

  • IsPullToRefreshEnabled: Pull To Refresh 사용 여부 설정

Pull To Refresh를 위한 ListView의 주요 이벤트

  • Refreshing: Pull To Refresh가 발동될 때 발생하는 이벤트

ListPage.xaml

<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="MyFirstXamarinApp.ListPage">
    <ListView x:Name="listView" IsPullToRefreshEnabled="true" Refreshing="Handle_Refreshing">
        <ListView.ItemTemplate>
            <DataTemplate>
                <TextCell Text="{Binding Name}" Detail="{Binding Status}"></TextCell>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</ContentPage>

ListPage.xaml.cs

using System.Collections.Generic;
using System.Collections.ObjectModel; // ObservableCollection
using MyFirstXamarinApp.Models;
using Xamarin.Forms;

namespace MyFirstXamarinApp
{
    public partial class ListPage : ContentPage
    {
        /* ListView에 추가할 List<T> 리턴. ObservableCollection<T>도 가능 */
        List<Contact> GetContacts(){
            return new List<Contact>
            {
                new Contact{Name="라이언", Status="안녕요?ㅎ", ImageUri="http://lorempixel.com/100/100/people/1"},
                new Contact{Name="어피치", Status="하이요?ㅎ", ImageUri="http://lorempixel.com/100/100/people/2"},
                new Contact{Name="프로도", Status="헬로우?ㅎ", ImageUri="http://lorempixel.com/100/100/people/3"}
            };
        }

        public ListPage()
        {
            InitializeComponent();
        }

        /* ListView Refreshing 이벤트 처리 메소드 */
        void Handle_Refreshing(object sender, System.EventArgs e)
        {
            /* ListView가 리프레시되면 새로운 List<T>를 리턴받음 */
            listView.ItemsSource = GetContacts();

            /* ListView 리프레시가 끝나면 리프레시 중단을 알려야 함 */
            /* 아래 둘 중 하나를 쓰면 됨. 취향의 차이(?) */
            listView.IsRefreshing = false;
            listView.EndRefresh();
        }
    }
}

실행 결과

좌: Refresh 전, 우: Refresh 후

 

Search Bar 만들기

SearchBar View를 사용하면 ListView의 내용을 검색할 수 있다.

SearchBar의 주요 속성

  • Placeholder: Search Bar의 값이 비어있을 때 기본적으로 보여줄 텍스트

SearchBar의 주요 이벤트

  • TextChanged: Search Bar의 값이 바꼈을 때 발생하는 이벤트

ListView.xaml

<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="MyFirstXamarinApp.ListPage">
    <StackLayout>
        <SearchBar Placeholder="Search..." TextChanged="Handle_TextChanged"></SearchBar>
        <ListView x:Name="listView">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <TextCell Text="{Binding Name}" Detail="{Binding Status}"></TextCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </StackLayout>
</ContentPage>

ListView.xaml.cs

using System.Collections.Generic;
using System.Linq;
using Xamarin.Forms;
using MyFirstXamarinApp.Models;

namespace MyFirstXamarinApp
{
    public partial class ListPage : ContentPage
    {
        public ListPage()
        {
            InitializeComponent();

            listView.ItemsSource = new List<Contact>
            {
                new Contact{Name="라이언", Status="안녕요?ㅎ", ImageUri="http://lorempixel.com/100/100/people/1"},
                new Contact{Name="어피치", Status="하이요?ㅎ", ImageUri="http://lorempixel.com/100/100/people/2"},
                new Contact{Name="프로도", Status="헬로우?ㅎ", ImageUri="http://lorempixel.com/100/100/people/3"}
            };
        }

        /* Search Bar 검색에 사용할 메소드 */
        IEnumerable<Contact> GetContacts(string searchText = null)
        {
            // 이 부분은 Hard Coding(데이터를 코드 내부에 직접 입력하는 것)
            // 보통 이렇게 사용하지는 않지만 일단 실습을 위해 사용...
            var contacts = new List<Contact>
            {
                new Contact{Name="라이언", Status="안녕요?ㅎ", ImageUri="http://lorempixel.com/100/100/people/1"},
                new Contact{Name="어피치", Status="하이요?ㅎ", ImageUri="http://lorempixel.com/100/100/people/2"},
                new Contact{Name="프로도", Status="헬로우?ㅎ", ImageUri="http://lorempixel.com/100/100/people/3"}
            };

            // 입력값이 없거나 공백인 경우 모든 값을 반환
            if (string.IsNullOrWhiteSpace(searchText))
                return contacts;


            // LINQ의 Where() 메소드는 IEnumerable 타입을 반환
            // 메소드의 리턴 형식을 List<T>로 선언한 후 ToList() 메소드를 사용하여 List<T>로 반환하거나
            //  - 예: return contacts.Where(c => c.Name.StartsWith(searchText)).ToList();
            // 순차 처리 방식만 사용한다면 IEnumerable 타입을 사용
            return contacts.Where(c => c.Name.StartsWith(searchText));
        }

        /* Search Bar TextChanged 이벤트 처리 메소드 */
        void Handle_TextChanged(object sender, Xamarin.Forms.TextChangedEventArgs e)
        {
            listView.ItemsSource = GetContacts(e.NewTextValue);
        }
    }
}

실행 화면