Item 이 Layout 에 따라 자동으로 배치되도록 해보자
ItemsView<TItemsLayout>
개발 배경
ListView
나 CollectionView
는 Xamarin 팀에서 제공하는 Layout 만 사용해야 하는 단점이 있습니다.
저는 이를 극복해보고자 CollectionView.ItemsLayout
에 저만의 레이아웃을 추가해보려고 했지만 실패했고, Custom Layout 을 활용해서 Item 을 배치할 수 있는ItemsView<TItemsLayout>
를 만들게 되었습니다.
글 순서는 프로젝트에 바로 사용하실 수 있도록 배치했습니다.
- 소스코드
- 활용방법
- 설명
ItemsView<TItemsLayout>
소스코드
[ContentProperty(nameof(ItemsLayout))]
public abstract class ItemsView<TItemsLayout>
: ContentView, IItemsElement
where TItemsLayout : Layout<View>, new()
{
#region ItemsSource
public static readonly BindableProperty ItemsSourceProperty = ItemsElement.ItemsSourceProperty;
public void OnItemsSourceChanged(IEnumerable newItemsSource)
{
ItemsLayout.Children.Clear();
if (newItemsSource != null && ItemTemplate != null)
{
var enumerator = newItemsSource.GetEnumerator();
while (enumerator.MoveNext())
{
var itemView = ItemTemplate.CreateContent() as View;
itemView.BindingContext = enumerator.Current;
ItemsLayout.Children.Add(itemView);
}
if (newItemsSource is INotifyCollectionChanged observableColleciton)
observableColleciton.CollectionChanged += OnItemSourceChanged;
}
}
public IEnumerable ItemsSource
{
get => GetValue(ItemsSourceProperty) as IEnumerable;
set => SetValue(ItemsSourceProperty, value);
}
#endregion
#region ItemTemplate
public static readonly BindableProperty ItemTemplateProperty = ItemsElement.ItemTemplateProperty;
public void OnItemTemplateChanged(DataTemplate newDataTemplate)
{
}
public DataTemplate ItemTemplate
{
get => GetValue(ItemTemplateProperty) as DataTemplate;
set => SetValue(ItemTemplateProperty, value);
}
#endregion
public TItemsLayout ItemsLayout { get; set; }
public ItemsView()
{
Content = ItemsLayout = new TItemsLayout();
}
private void OnItemSourceChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Reset)
ItemsLayout.Children.Clear();
// Item 제거
if (e.OldItems != null && 0 < e.OldItems.Count)
{
var removedItems = ItemsLayout.Children
.Where(item => e.OldItems.Contains(item.BindingContext))
.Select(item => item);
foreach (var item in removedItems.ToList())
ItemsLayout.Children.Remove(item);
}
// Item 추가
if (e.NewItems != null && 0 < e.NewItems.Count)
{
foreach (var newItem in e.NewItems)
{
var itemView = ItemTemplate.CreateContent() as View;
itemView.BindingContext = newItem;
ItemsLayout.Children.Add(itemView);
}
}
}
}
Xaml 에서 ItemsView<TItemsLayout>
사용하는 방법
1번 레이아웃 속성 활용이 불가능한 사용방법
ItemsView<TItemsLayout>
를 단순하게 사용하면 다음과 같이 사용할 수 있습니다. 하지만 BindableProperty
를 만들 수 없기 때문에 굉장히 제한적입니다.
<local:ItemsView
x:TypeArguments="StackLayout"
ItemsSource="{Binding ItemsSource}">
<local:ItemsView.ItemTemplate>
<DataTemplate>
<Label Text="{Binding Text}"/>
</DataTemplate>
</local:ItemsView.ItemTemplate>
</local:ItemsView>
2번 레이아웃 속성 활용이 가능한 사용방법
1번과 달리 ItemsView<TItemsLayout>
상속해서 특정 레이아웃으로 고정한 View 를 만들고 BindableProperty
추가해 사용하는 방법도 있습니다.
public class StackItemsView : ItemsView<StackLayout>
{
#region Orientation
public static readonly BindableProperty OrientationProperty
= BindableProperty.Create(nameof(Orientation), typeof(StackOrientation), typeof(StackItemsView), StackOrientation.Vertical,
propertyChanged: (bindable, oldValue, newValue) => (bindable as StackItemsView).ItemsLayout.Orientation = (StackOrientation)newValue);
public StackOrientation Orientation
{
get => (StackOrientation)GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value);
}
#endregion
#region Spacing
public static readonly BindableProperty SpacingProperty
= BindableProperty.Create(nameof(Spacing), typeof(double), typeof(StackItemsView), 0.0D,
propertyChanged: (bindable, oldValue, newValue) => (bindable as StackItemsView).ItemsLayout.Spacing = (double)newValue);
public double Spacing
{
get => (double)GetValue(SpacingProperty);
set => SetValue(SpacingProperty, value);
}
#endregion
public StackItemsView()
{
ItemsLayout.Orientation = StackOrientation.Vertical;
ItemsLayout.Spacing = 0.0D;
}
}
<local:StackItemsView
Orientation="Horizontal"
Spacing="10"
ItemsSource="{Binding ItemsSource}">
<local:StackItemsView.ItemTemplate>
<DataTemplate>
<Label Text="{Binding Text}"/>
</DataTemplate>
</local:StackItemsView.ItemTemplate>
</local:StackItemsView>
ItemsView<TItemsLayout>
를 ResponsiveLayout 과 함께 사용
ResponsiveLayout
과 ItemsView<TItemsLayout>
를 합쳐서 만든 ResponsiveItemsView
를 사용하면 화면 크기에 따라 Layout 이 변화하면서 Item 을 재배치 해주는 유용한 View 를 만들 수 있습니다.
public class ResponsiveItemsView : ItemsView<ResponsiveLayout>, IGridSpacingElement
{
#region RowSpacing
public static readonly BindableProperty RowSpacingProperty = GridSpacingElement.RowSpacingProperty;
public void OnRowSpacingChanged(double newRowSpacing)
=> ItemsLayout.RowSpacing = newRowSpacing;
public double RowSpacing
{
get => (double)GetValue(RowSpacingProperty);
set => SetValue(RowSpacingProperty, value);
}
#endregion
#region ColumnSpacing
public static readonly BindableProperty ColumnSpacingProperty = GridSpacingElement.ColumnSpacingProperty;
public void OnColumnSpacingChanged(double newColumnSpacing)
=> ItemsLayout.ColumnSpacing = newColumnSpacing;
public double ColumnSpacing
{
get => (double)GetValue(ColumnSpacingProperty);
set => SetValue(ColumnSpacingProperty, value);
}
#endregion
}
Element 패턴을 적용한 GridSpacingElement
interface IGridSpacingElement
{
double RowSpacing { get; set; }
void OnRowSpacingChanged(double newRowSpacing);
double ColumnSpacing { get; set; }
void OnColumnSpacingChanged(double newColumnSpacing);
}
static class GridSpacingElement
{
public static readonly BindableProperty RowSpacingProperty
= BindableProperty.Create(
propertyName: nameof(IGridSpacingElement.RowSpacing),
returnType: typeof(double),
declaringType: typeof(IGridSpacingElement),
defaultValue: 0.0D,
defaultBindingMode: BindingMode.OneWay,
propertyChanged: OnRowSpacingChanged);
private static void OnRowSpacingChanged(BindableObject bindable, object oldValue, object newValue)
=> (bindable as IGridSpacingElement).OnRowSpacingChanged((double)newValue);
public static readonly BindableProperty ColumnSpacingProperty
= BindableProperty.Create(
propertyName: nameof(IGridSpacingElement.ColumnSpacing),
returnType: typeof(double),
declaringType: typeof(IGridSpacingElement),
defaultValue: 0.0D,
defaultBindingMode: BindingMode.OneWay,
propertyChanged: OnColumnSpacingChanged);
private static void OnColumnSpacingChanged(BindableObject bindable, object oldValue, object newValue)
=> (bindable as IGridSpacingElement).OnColumnSpacingChanged((double)newValue);
}
ItemsView<TItemsLayout>
설명
ItemsView<TItemsLayout>
클래스 정의
public class ItemsView<TItemsLayout>
: ContentView, IItemsElement
where TItemsLayout : Layout<View>, new()
ItemsView<TItemsLayout>
는 ContentView
를 상속받았으며 Element 패턴을 구현한 ItemsElement
static 클래스와 IItemsElement
인터페이스를 활용해서 ItemsSourceProperty
와 ItemTemplateProperty
속성을 구현합니다.
interface IItemsElement
{
public IEnumerable ItemsSource { get; set; }
void OnItemsSourceChanged(IEnumerable newItemsSource);
public DataTemplate ItemTemplate { get; set; }
void OnItemTemplateChanged(DataTemplate newDataTemplate);
}
static class ItemsElement
{
public static readonly BindableProperty ItemsSourceProperty
= BindableProperty.Create(
propertyName: nameof(IItemsElement.ItemsSource),
returnType: typeof(IEnumerable),
declaringType: typeof(IItemsElement),
defaultValue: null,
defaultBindingMode: BindingMode.OneWay,
propertyChanged: OnItemsSourceChanged);
private static void OnItemsSourceChanged(BindableObject bindable, object oldValue, object newValue)
=> (bindable as IItemsElement).OnItemsSourceChanged(newValue as IEnumerable);
public static readonly BindableProperty ItemTemplateProperty
= BindableProperty.Create(
propertyName: nameof(IItemsElement.ItemTemplate),
returnType: typeof(DataTemplate),
declaringType: typeof(IItemsElement),
defaultValue: null,
defaultBindingMode: BindingMode.OneWay,
propertyChanged: OnItemTemplateChanged);
private static void OnItemTemplateChanged(BindableObject bindable, object oldValue, object newValue)
=> (bindable as IItemsElement).OnItemTemplateChanged(newValue as DataTemplate);
}
그리고 ItemsView<TItemsLayout>
에 사용할 generic 타입을 다음과 같이 정의했습니다.
<TItemsLayout> where TItemsLayout : Layout\<View\>, new()
TItemsLayout
은 Custom Layout 을 사용해야 하니 당연히 Layout<View>
을 상속하고, ItemsView<TItemsLayout>
생성자에서 TItemsLayout
을 생성하기 위해 new()
제약 조건을 지정했습니다. new()
를 지정하면 TItemsLayout
타입은 반드시 매개 변수가 없는 생성자가 있어야만 합니다.
ItemsSource
와 ItemsTemplate
활용
ItemsSource
는 View 의 BindingContext 가 될 ViewModel 이 바인딩되고 ItemsTemplate
은 Layout 에 배치할 Item 의 DataTemplate
을 바인딩합니다. 그래서 ItemsSource
가 바인딩 되면 ItemsView<TItemsLayout>
의 ItemsLayout
에 새로운 View 를 만들어서 추가합니다.
public void OnItemsSourceChanged(IEnumerable newItemsSource)
{
// ItemsSource 가 달라지게 되었으니 기존 View 를 모두 제거합니다.
ItemsLayout.Children.Clear();
if (newItemsSource != null && ItemTemplate != null)
{
var enumerator = newItemsSource.GetEnumerator();
while (enumerator.MoveNext())
{
// ItemTemplate 을 활용해 새로운 View 를 생성합니다.
var itemView = ItemTemplate.CreateContent() as View;
// 새롭게 생성된 View 에 현재 ItemSource 를 BindingContext 로 할당합니다.
itemView.BindingContext = enumerator.Current;
// 완성된 View 를 레이아웃에 추가합니다.
ItemsLayout.Children.Add(itemView);
}
// ItemsSource 가 변하면 View 도 변화해야 하므로 CollectionChanged 이벤트를 구독합니다.
if (newItemsSource is INotifyCollectionChanged observableColleciton)
observableColleciton.CollectionChanged += OnItemSourceChanged;
}
}
만약 바인딩된 ItemsSource
가 INotifyCollectionChanged
을 구현한 ObservableColleciton
일 경우 INotifyCollectionChanged.CollectionChanged
이벤트를 구독해서 ItemsSource
가 변경되었을 때 ItemsView<TItemsLayout>
도 변경될 수 있도록 합니다.
private void OnItemSourceChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Reset)
ItemsLayout.Children.Clear();
// Item 제거
if (e.OldItems != null && 0 < e.OldItems.Count)
{
// ItemsSource 에서 제거된 Item 과 연결된 View 를 제거합니다.
var removedItems = ItemsLayout.Children
.Where(item => e.OldItems.Contains(item.BindingContext))
.Select(item => item);
foreach (var item in removedItems.ToList())
ItemsLayout.Children.Remove(item);
}
if (e.NewItems != null && 0 < e.NewItems.Count)
{
// Item 추가
foreach (var newItem in e.NewItems)
{
// ItemsSource 에 새로운 Item 이 추가되었다면 새로운 View 를 만들어서 ItemsLayout 에 추가합니다.
var itemView = ItemTemplate.CreateContent() as View;
itemView.BindingContext = newItem;
ItemsLayout.Children.Add(itemView);
}
}
}
'Develop > MAUI 가이드' 카테고리의 다른 글
[Xamarin] View 안에 View 를 주입할 수 있는 View 를 작성해보자 (0) | 2020.12.24 |
---|---|
[Xamarin] BindableProperty 를 Override 해보자 (0) | 2020.12.24 |
[Xamarin] 화면 크기에 따라 변화하는 ResponsiveLayout 을 만들어 보자 (0) | 2020.12.23 |
[Xamarin] BindableProperty 구현하고 관리하는 방법 (0) | 2020.12.17 |
[Xamarin] Effects 로 Platform-Specific 하게 구현하기 (0) | 2020.11.17 |
꾸준히 노력하는 개발자 "김예건" 입니다.