티스토리 뷰

반응형

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)

 

포팅시 어려웠던 부분?

  1. Entity Framework Core 버전이 2.1에서 3.x 버전으로 변경되면서 발생한 문제를 해결하는데  시간이 오래 걸렸습니다. 변경 사항은 여기 참고하시기 바랍니다.
  2. Sqlite Entity Framework Core 이용해서 앱에서 직접 접근해서 사용하는 방식이 Wasm, iOS, macOS 등에서 원활하지 않아서, Web API 통해서만 사용할 있도록 수정하는데 시간이 걸렸습니다.
  3. 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” 사용해야 합니다.
<!--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

 

반응형
댓글