티스토리 뷰
반응형
2020년은 오프라인 비지니스 환경에서 언텍트 비지니스 환경으로 변화하는 변곡점이라고 할 수 있을 것 같습니다. 그에 따라 이제는 PC 뿐만 아니라 다양한 모바일 디바이스에서 실행되는 크로스플랫폼 개발 환경이 더욱더 발전을 할 것이라고 생각됩니다.
2018년 5월 최초로 공개된 Uno Platform은 크로스 플랫폼에서 실행되는 비지니스 응용 프로그램을 개발하는 가장 빠른 방법이라고 생각하며, 이 포스트에서 Microsoft Contoso UWP앱을 Uno Platform Prism으로 포팅할 때 참고할 사항이나 필요한 기술에 대해서 이야기를 하려고 합니다. Uno Platform에 대한 더 자세한 사항은 여기를 참고하시기 바랍니다.
Contoso UWP app
https://github.com/microsoft/Windows-appsample-customers-orders-database
UnoContoso app – Uno Platform
Uno.Samples/UI/UnoContoso at master · unoplatform/Uno.Samples (github.com)
포팅시 어려웠던 부분?
- Entity Framework Core 버전이 2.1에서 3.x 버전으로 변경되면서 발생한 문제를 해결하는데 시간이 오래 걸렸습니다. 변경 사항은 여기를 참고하시기 바랍니다.
- Sqlite를 Entity Framework Core를 이용해서 앱에서 직접 접근해서 사용하는 방식이 Wasm, iOS, macOS 등에서 원활하지 않아서, Web API를 통해서만 사용할 수 있도록 수정하는데 시간이 좀 걸렸습니다.
- Contoso UWP앱이 Event-driven 방식을 사용하고 있어서, 그 부분을 MVVM Pattern으로 변경하는 부분도 시간이 걸렸습니다.
프로젝트에 대한 간단한 설명
- UnoContoso.Droid : 안드로이드 해더 프로젝트
- UnoContoso.iOS : iOS 헤더 프로젝트
- UnoContoso.macOS : macOS 헤더 프로젝트
- UnoContoso.Uwp : Uwp 헤더 프로젝트
- UnoContoso.Wasm : Wasm 헤더 프로젝트
- UnoContoso.Models : 모델 프로젝트, .NET Standard 2.0
- UnoContoso.Repository : 레파지토리 프로젝트, .NET Standard 2.0
- UnoContoso.Service : 서비스 프로젝트, .NET Core 3.1
- UnoContoso.Shared : 공유 프로젝트
Tips
- NavigationView
- AlwaysShowHeader=”False” :상단에 헤더를 표시하지 않습니다. 헤더를 표시하는 것 보다 표시 하지 않는 것이 아래의 이유로 인해서 개발이 용이 합니다.
- iOS의 경우 헤더의 위치가 다른 플랫폼과 다르게 위로 올라가서 표시됩니다.
- Windows, Wasm, macOS에서는 헤더 위치에 CommandBar를 표시하고, Android, iOS에서는 화면 하단에 표시하기 때문에, NavigationView.HeaderTemplate을 사용이 어렵습니다.
- NavigationViewBehavior : NavigationView에 표시되는 메뉴를 동적으로 구현하고, 선택된 Menu를 ViewModel에 바인딩하기 위해서 Behavior를 만들었습니다. Behavior의 일반적인 사항은 여기를 참고하시기 바랍니다.
- ContentControl : Prism에서 일반적으로 Region을 지정하는 컨트롤입니다. 이 컨트롤을 통해서 View가 네비게이션 됩니다. 더 자세한 사항은 Prism의 RegionNavigation을 참고하시기 바랍니다.
- 이 컨트롤의 Padding은 각 화면의 기본 Margin입니다.
- iOS의 경우 ios:Padding=”10,20,0,0” 을 사용해야 합니다.
- AlwaysShowHeader=”False” :상단에 헤더를 표시하지 않습니다. 헤더를 표시하는 것 보다 표시 하지 않는 것이 아래의 이유로 인해서 개발이 용이 합니다.
<!--Code1-->
<NavigationView
IsBackButtonVisible="Collapsed"
OpenPaneLength="160"
IsSettingsVisible="False"
AlwaysShowHeader="False"
IsTabStop="False">
<i:Interaction.Behaviors>
<behaviors:NavigationViewBehavior
MenuItems="{Binding Menus}"
SelectedMenuItem="{Binding SelectedItem, Mode=TwoWay}"/>
</i:Interaction.Behaviors>
<ContentControl
prismRegions:RegionManager.RegionName="ContentRegion"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
not_ios:Padding="10,0,0,0"
ios:Padding="10,20,0,0"/>
</NavigationView>
- x:Bind
- Android와 iOS 앱의 성능 향상을 위해서 x:Bind를 주로 사용했습니다.
- x:Bind를 사용하기 위해서 code behind에서ViewModel이라는 프로퍼티를 추가합니다. (Code2-1)
- x:Bind의 기본 mode는 OneTime입니다. 그래서, 프로퍼티의 데이터가 변경되는 곳이라면 mode=OneWay를 반드시 추가해야 합니다.
- 컨트롤의 이벤트와 뷰모델의 메소드를 x:Bind를 직접 연결하는 방법은 사용할 수 없습니다.
- IValueConverter를 이용하는 경우 Android에서 x:Bind는 사용할 수 없기 때문에, Binding을 사용합니다. (Code2-2)
//Code2-1
public CustomerListViewModel ViewModel
{
get { return DataContext as CustomerListViewModel; }
}
<!--Code2-2-->
<TextBlock
Grid.Column="0"
Margin="0" Padding="0"
Text="{x:Bind Product.Name}" />
<TextBlock
Grid.Column="2"
Margin="0" Padding="0"
HorizontalAlignment="Right"
Text="{Binding Product.ListPrice,
Converter={StaticResource StringFormatConverter},
ConverterParameter='{}{0:n}'}" />
- Prism
- View와 ViewModel을 자동으로 연결해 줍니다. prismMvvm:ViewModelLocator.AutowireViewModel="True" 코드를 참고 합니다. 자세한 사항은 여기를 참고 합니다.(Code3-1)
- RegionNavigation을 사용하기 위해서는 App.xaml.cs → void RegisterTypes() 메소드에 RegisterForNavigation을 이용해서 등록해야 합니다. (Code3-2)
- DialogService를 사용하는 화면은 View와 ViewModel을 수동으로 연결 합니다. App.xaml.cs -} void RegisterTypes() 메소드에 있는 코드를 참고 합니다. 더 자세한 사항은 여기를 참고 합니다. (Code3-3)
- DelegateCommand의 ObservesProperty를 이용하면, Command의 사용여부를 쉽게 변경할 수 있습니다. 더 자세한 사항은 여기를 참고 합니다. (Code3-4)
- EventAggregator를 이용하면 뷰모델과 뷰모델 사이에 커뮤니케이션을 할 수 있습니다. 이는 Loosely Coupled 연결입니다. 더 자세한 사항은 여기를 참고 합니다. (Code3-5)
<!--Code3-1-->
<UserControl
x:Class="UnoContoso.Views.OrderDetailView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:UnoContoso.Views"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:i="using:Microsoft.Xaml.Interactivity"
xmlns:ic="using:Microsoft.Xaml.Interactions.Core"
xmlns:toolkit="using:Microsoft.Toolkit.Uwp.UI.Controls"
xmlns:prismMvvm="using:Prism.Mvvm"
prismMvvm:ViewModelLocator.AutowireViewModel="True"
xmlns:uc="using:UnoContoso.UserControls"
xmlns:stateTriggers="using:UnoContoso.StateTriggers"
xmlns:models="using:UnoContoso.Models"
x:Name="Root"
mc:Ignorable="d">
//Code3-2
containerRegistry.RegisterForNavigation<CustomerListView>();
containerRegistry.RegisterForNavigation<CustomerDetailView>();
containerRegistry.RegisterForNavigation<HomeView>();
containerRegistry.RegisterForNavigation<OrderListView>();
containerRegistry.RegisterForNavigation<OrderDetailView>();
//Code3-3
containerRegistry.RegisterDialog<MessageControl, MessageViewModel>();
containerRegistry.RegisterDialog<ConfirmControl, ConfirmViewModel>();
//Code3-4
ViewDetailCommand = new DelegateCommand(OnViewDetail,
() => SelectedCustomer != null)
.ObservesProperty(() => SelectedCustomer);
//Code3-5
Step 1. CustomerListViewModel.cs
EventAggregator.GetEvent<CustomerEvent>()
.Subscribe(ReceivedCustomerEvnet, false);
Step 2. CustomerDetailViewModel.cs
EventAggregator.GetEvent<CustomerEvent>()
.Publish(new EventArgs.CustomerEventArgs
{
Changes = change,
Customer = Customer.Model
});
Step 3. CustomerListViewModel.cs
private void ReceivedCustomerEvnet(CustomerEventArgs obj)
{
switch (obj.Changes)
{
case Enums.EntityChanges.None:
break;
case Enums.EntityChanges.Changed:
{
var customer = _allCustomers
.FirstOrDefault(c => c.Model.Id == obj.Customer.Id);
if (customer == null) return;
customer.Model = obj.Customer;
customer.UpdateProperty();
}
break;
case Enums.EntityChanges.Added:
{
var customer = new CustomerWrapper(_contosoRepository, obj.Customer);
_allCustomers.Add(customer);
}
break;
case Enums.EntityChanges.Deleted:
{
var customer = _allCustomers
.FirstOrDefault(c => c.Model.Id == obj.Customer.Id);
if (customer == null) return;
_allCustomers.Remove(customer);
}
break;
}
}
- RelativePanel
- 각 컨트롤을 관계에 의해 배치 합니다. 자세한 사항은 여기를 참고 하시기 바랍니다.
- VisualState를 이용해서 상단에 위치한 CommandBar의 위치를 하단으로 변경할 때 CommandBar 컨트롤에 RelativePanel.LeftOf와 RelativePanel.RightOf에 입력되어있는 이름을 지워주어야 하단으로 이동이 가능합니다. 이 부분은 UWP에서의 동작과는 다르게 동작합니다.
<!--Code4-->
<VisualState>
<VisualState.StateTriggers>
<stateTriggers:MobileScreenTrigger />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="mainCommandBar.(RelativePanel.AlignBottomWithPanel)" Value="True" />
<Setter Target="mainCommandBar.(RelativePanel.AlignLeftWithPanel)" Value="True" />
<Setter Target="mainCommandBar.(RelativePanel.AlignRightWithPanel)" Value="True" />
<Setter Target="mainCommandBar.(RelativePanel.LeftOf)" Value="" />
<Setter Target="mainCommandBar.(RelativePanel.RightOf)" Value="" />
<Setter Target="mainCommandBar.HorizontalAlignment" Value="Stretch" />
<Setter Target="PageTitle.Margin" Value="30,4,0,0"/>
<Setter Target="CustomerSearchBox.Width" Value="240" />
<Setter Target="RootDataGrid.Margin" Value="0,10,0,40" />
<Setter Target="CustomerListMobileView.Visibility" Value="Visible"/>
<Setter Target="CustomerDataGrid.Visibility" Value="Collapsed"/>
</VisualState.Setters>
</VisualState>
- CommandBar
- 커맨드 버튼을 배치하는 컨트롤 입니다. 기본적인 사항은 여기를 참고하시기 바랍니다.
- CommandBarBehavior는 크기가 변하면 DefaultLabelPosition을 Right에서 Bottom으로 이동 시키기 위해서 추가한 Behavior입니다.
- DefaultLabelPosition 프로퍼티가 Uno에서 미구현 프로퍼티이기 때문에, 대부분의 플랫폼에서는 기본값 Bottm을 가지고 있습니다.
<!--Code5-->
<CommandBar
x:Name="mainCommandBar"
Background="White"
HorizontalAlignment="Right"
DefaultLabelPosition="Right"
RelativePanel.LeftOf="CustomerSearchBox"
RelativePanel.RightOf="PageTitle">
<i:Interaction.Behaviors>
<behaviors:CommandBarBehavior/>
</i:Interaction.Behaviors>
<AppBarButton
Icon="Contact"
Label="View details"
ToolTipService.ToolTip="View details"
Command="{x:Bind ViewModel.ViewDetailCommand}"/>
<!--skip lines-->
<AppBarButton
Icon="Refresh"
Label="Sync"
ToolTipService.ToolTip="Sync with server"
Command="{x:Bind ViewModel.SyncCommand}"/>
</CommandBar>
- CollapsibleSearchBox
- UserControl을 이용해서 만든 검색용 컨트롤입니다.
- Uno에서 그래픽 아이콘은 SymbolIcon 컨트롤을 이용해야, 각 플랫폼에서도 정상적으로 아이콘이 출력됩니다.
<!--Code6-->
<ToggleButton
x:Name="searchButton"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Background="Transparent"
Checked="SearchButton_Checked"
Visibility="Collapsed"
Padding="4">
<SymbolIcon Symbol="Find" />
</ToggleButton>
- ListView
- 모바일 디바이스에서는 화면에 출력되는 항목의 갯수와 가로 스크롤바의 유무에 따라서 세로 스크롤 성능에 영향을 미치기 때문에 DataGrid보다는 ListView를 이용하는 것이 좋습니다. 더 자세한 사항은 여기를 참고 합니다.
- macOS에서는 아직 DataGrid를 사용할 수 없기 때문에 ListView를 사용합니다. 하지만, macOS의 ListView도 컨트롤 내부에 ScrollView가 없기 때문에 세로 스크롤이 불가능한 것으로 보입니다. macOS 지원은 점점 추가될 것으로 생각합니다.
- DataGrid
- Data를 Grid 형태로 보여주며, 수정도 가능한 컨트롤이며, Windows Community Toolkit을 설치해야 사용할 수 있습니다. 더 자세한 사항은 여기를 참고 합니다.
- DataGridBehavior는 Sort기능과 마우스 오른쪽 버튼을 지원하기 위해서 만들었습니다.
- 컬럼에 프로퍼티를 바인딩 할때 x:Bind를 사용할 수 없습니다.
<!--Code7-->
<toolkit:DataGrid
x:Name="OrderListDataGrid" BorderThickness="0"
CanUserReorderColumns="False" CanUserResizeColumns="False"
GridLinesVisibility="None" IsReadOnly="True"
AutoGenerateColumns="False" Margin="0,10,0,0"
ItemsSource="{x:Bind ViewModel.Orders, Mode=OneWay}"
SelectedItem="{x:Bind ViewModel.SelectedOrder, Mode=TwoWay}"
ContextFlyout="{StaticResource DataGridContextMenu}">
<i:Interaction.Behaviors>
<behaviors:DataGridBehavior/>
</i:Interaction.Behaviors>
<toolkit:DataGrid.Columns>
<toolkit:DataGridTextColumn
Header="Invoice" Tag="InvoiceNumber" Binding="{Binding InvoiceNumber}"/>
<toolkit:DataGridTextColumn
Header="Customer" Tag="CustomerName" Binding="{Binding CustomerName}"/>
<toolkit:DataGridTemplateColumn
Header="Date" Tag="DatePlaced">
<toolkit:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock
VerticalAlignment="Center" Margin="12,0"
Text="{Binding DatePlaced, Mode=OneWay,
Converter={StaticResource StringFormatConverter},
ConverterParameter='{}{0:d}'}" />
</DataTemplate>
</toolkit:DataGridTemplateColumn.CellTemplate>
</toolkit:DataGridTemplateColumn>
<!-- Skip lines -->
<toolkit:DataGridTextColumn
Header="Status" Binding="{Binding Status}"/>
</toolkit:DataGrid.Columns>
</toolkit:DataGrid>
- VisualState
- 특정 상태일 때 UI 요소의 시각적 모양을 지정할 수 있는 기능입니다. 자세한 사항은 여기를 참고 합니다.
- AdaptiveTrigger : 윈도우 속성을 기반으로 시각적 상태를 선언하는 트리거 입니다. 자세한 사항은 여기를 참고 합니다.
- VisualState를 이용해서 디자인 작업을 할 때, 기본 크기에 대한 디자인은 화면에 왼쪽 상단에서8” Tablet(1280x800)을 선택하고, 최소 크기에 대한 디자인은 6”Phone(1920x1080)을 선택해서 작업하면 약간 편하게 작업할 수 있습니다.
- 최소 크기인 경우 PageTitle 컨트롤의 왼쪽에 30의 Margin을 추가해서 햄버거 버튼과 겹치는 현상을 방지 합니다.
- MobileScreenTrigger는 윈도우 크기가 변경될 때 실행 중인 디바이스의 이름을 가지고 오고, Mobile이라는 이름을 포함하면 SetActive()를 이용해서 VisualState를 변경합니다. UIViewSettings.GetForCurrentView().UserInteractionMode는 Uno에서 미구현 상태라 주석 처리를 했습니다.
<!--Code8-->
<VisualStateManager.VisualStateGroups>
<VisualStateGroup>
<VisualState>
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="{StaticResource LargeWindowSnapPoint}" />
</VisualState.StateTriggers>
</VisualState>
<VisualState>
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="{StaticResource MediumWindowSnapPoint}" />
</VisualState.StateTriggers>
</VisualState>
<VisualState>
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="{StaticResource MinWindowSnapPoint}" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="PageTitle.Margin" Value="30,4,0,0"/>
</VisualState.Setters>
</VisualState>
<VisualState>
<VisualState.StateTriggers>
<stateTriggers:MobileScreenTrigger />
</VisualState.StateTriggers>
<VisualState.Setters>
<!-- Skip lines -->
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
- BackButton
- 뒤로가기 버튼입니다. GoBackCommand 커맨드는 ViewModelBase에 선언되어 있습니다.
<!--Code9-->
<Button
x:Name="BackButton"
Style="{StaticResource NavigationBackButtonNormalStyle}"
Command="{x:Bind ViewModel.GoBackCommand}"/>
- TextBox
- 일반 텍스트를 표시하고 편집하는 데 사용하는 컨트롤 입니다. 자세한 사항은 여기를 참고 합니다.
- Text 프로퍼티에 TwoWay바인딩을 할 때는 UpdateSourceTrigger=PropertyChanged를 이용하는 것이 좋습니다. 왜냐하면, 내용을 수정하면 바로 바인딩된 프로퍼티가 수정되기 때문입니다. 기본값은 LostFocus입니다.
<!--Code10-->
<TextBox
x:Name="FirstName"
MinWidth="120"
Margin="0,8,16,8"
Header="First name"
IsReadOnly="{x:Bind ViewModel.Customer.IsInEdit,
Converter={StaticResource BoolNegationConverter}, Mode=OneWay}"
RelativePanel.AlignLeftWithPanel="True"
Text="{x:Bind ViewModel.Customer.FirstName, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}" />
- InvoiceTemplate (DataTemplate)
- 데이터 개체의 시각적 구조를 만드는데 사용합니다. 자세한 사항은 여기를 참고 합니다.
- HyperlinkButton이 1개가 존재하며 Command가 win과 not_win으로 나누어져 있습니다. 각 플랫폼별 xaml 코드를 적용하는 방법은 여기를 참고 합니다.
- Windows에서는 win:Command="{Binding Source={StaticResource ViewModelElement}, Path=ViewModel.ViewInvoiceCommand}"를 사용해야 Command가 실행됩니다.
- Windows가 아닌 경우에는 not_win:Command="{Binding ElementName=DataGrid, Path=DataContext.ViewInvoiceCommand}"를 사용해야 Command가 실행됩니다.
- Windows에서 ElementName=DataGrid를 이용하는 방법이 않되는 이유는 모릅니다.
<!--Code11-->
<DataTemplate x:Key="InvoiceTemplate">
<HyperlinkButton
Content="{Binding InvoiceNumber}"
Margin="12,0"
win:Command="{Binding Source={StaticResource ViewModelElement},
Path=ViewModel.ViewInvoiceCommand}"
not_win:Command="{Binding ElementName=DataGrid,
Path=DataContext.ViewInvoiceCommand}"
CommandParameter="{Binding}"/>
</DataTemplate>
- Etc
- 만약 솔루션에 .Net Standard 프로젝트를 포함하고, 사용한다면, Wasm 프로젝트의 LinkerConfig.xml 파일에 해당 내용을 추가해야 합니다.
<!—LinkerConfig.xml -->
<linker>
<assembly fullname="UnoContoso.Wasm" />
<assembly fullname="Uno.UI" />
<assembly fullname="System.Core">
<!-- This is required by JSon.NET and any expression.Compile caller -->
<type fullname="System.Linq.Expressions*" />
</assembly>
<assembly fullname="UnoContoso.Models"/>
<assembly fullname="UnoContoso.Repository"/>
</linker>
이상으로 작업을 하면서 알게된 몇가지 팁들을 정리했습니다. 앞으로도 좋은 정보가 있으면 꾸준히 올리도록 하겠습니다.
Facebook : https://www.facebook.com/kaki104
Twitter : https://twitter.com/kaki104
Youtube : http://youtube.com/FutureOfDotNet
반응형
'Uno Platform' 카테고리의 다른 글
Empty path name is not legal. 에러 해결 (0) | 2020.12.17 |
---|---|
Uno Platform 3.3 – Day 0 support for WinUI 3 Preview 3, Android 11 (0) | 2020.11.23 |
Can I make business apps with the Uno Platform? (0) | 2020.10.30 |
Running UWP on Linux with Uno Platform (0) | 2020.08.27 |
Creating a simple RSS reader using Uno Platform part2 (0) | 2020.07.07 |
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
링크
TAG
- ef core
- Behavior
- dotNETconf
- LINQ
- kiosk
- IOT
- MVVM
- uno-platform
- .net 5.0
- Build 2016
- PRISM
- WPF
- .net
- #prism
- Bot Framework
- windows 11
- #Windows Template Studio
- visual studio 2019
- #uwp
- #MVVM
- Visual Studio 2022
- ComboBox
- Cross-platform
- C#
- Microsoft
- uno platform
- UWP
- Windows 10
- Always Encrypted
- XAML
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
글 보관함