엔터프라이즈 앱 탐색

참고 항목

이 전자책은 2017년 봄에 게시되었으며 그 이후로 업데이트되지 않았습니다. 이 책에는 귀중한 기본 많이 있지만 일부 자료는 구식입니다.

Xamarin.Forms 에는 일반적으로 사용자가 UI와 상호 작용하거나 내부 논리 기반 상태 변경의 결과로 앱 자체에서 발생하는 페이지 탐색에 대한 지원이 포함됩니다. 그러나 MVVM(Model-View-ViewModel) 패턴을 사용하는 앱에서는 다음과 같은 어려운 문제를 해결해야 하므로 탐색이 복잡할 수 있습니다.

  • 뷰 간의 긴밀한 결합 및 종속성을 도입하지 않는 방법을 사용하여 탐색할 뷰를 식별하는 방법입니다.
  • 탐색할 뷰가 인스턴스화되고 초기화되는 프로세스를 조정하는 방법입니다. MVVM을 사용하는 경우 뷰 및 뷰 모델을 인스턴스화하고 뷰의 바인딩 컨텍스트를 통해 서로 연결해야 합니다. 앱이 종속성 주입 컨테이너를 사용하는 경우 뷰 및 뷰 모델의 인스턴스화에는 특정 생성 메커니즘이 필요할 수 있습니다.
  • 뷰 우선 탐색을 수행할지 또는 모델 우선 탐색을 볼지 여부입니다. 뷰 우선 탐색을 사용하면 탐색할 페이지가 뷰 형식의 이름을 참조합니다. 탐색하는 동안 지정된 뷰는 해당 뷰 모델 및 기타 종속 서비스와 함께 인스턴스화됩니다. 다른 방법은 보기 모델 우선 탐색을 사용하는 것입니다. 여기서 탐색할 페이지는 보기 모델 형식의 이름을 참조합니다.
  • 보기 및 보기 모델에서 앱의 탐색 동작을 클린 구분하는 방법입니다. MVVM 패턴은 앱의 UI와 프레젠테이션 및 비즈니스 논리를 구분합니다. 그러나 앱의 탐색 동작은 앱의 UI 및 프레젠테이션 부분에 걸쳐 있는 경우가 많습니다. 사용자는 뷰에서 탐색을 시작하는 경우가 많으며 탐색의 결과로 뷰가 대체됩니다. 그러나 보기 모델 내에서 탐색을 시작하거나 조정해야 하는 경우가 많습니다.
  • 초기화를 위해 탐색하는 동안 매개 변수를 전달하는 방법입니다. 예를 들어 사용자가 뷰로 이동하여 주문 세부 정보를 업데이트하는 경우 올바른 데이터를 표시할 수 있도록 주문 데이터를 뷰에 전달해야 합니다.
  • 특정 비즈니스 규칙이 준수되도록 탐색을 조정하는 방법입니다. 예를 들어 뷰에서 벗어나기 전에 잘못된 데이터를 수정하거나 뷰 내에서 변경한 데이터를 제출 또는 삭제하라는 메시지가 사용자에게 표시될 수 있습니다.

이 장에서는 모델 우선 보기 페이지 탐색을 NavigationService 수행하는 데 사용되는 클래스를 제시하여 이러한 문제를 해결합니다.

참고 항목

앱에서 사용하는 것은 NavigationService ContentPage 인스턴스 간의 계층적 탐색만 수행하도록 설계되었습니다. 서비스를 사용하여 다른 페이지 형식 간을 탐색하면 예기치 않은 동작이 발생할 수 있습니다.

탐색 논리는 뷰의 코드 숨김 또는 데이터 바인딩된 뷰 모델에 상주할 수 있습니다. 보기에 탐색 논리를 배치하는 것이 가장 간단한 방법일 수 있지만 단위 테스트를 통해 쉽게 테스트할 수 없습니다. 뷰 모델 클래스에 탐색 논리를 배치하면 단위 테스트를 통해 논리를 실행할 수 있습니다. 또한 뷰 모델은 특정 비즈니스 규칙이 적용되도록 탐색을 제어하는 논리를 구현할 수 있습니다. 예를 들어 앱은 사용자가 입력한 데이터가 유효한지 확인하지 않으면 페이지에서 벗어나는 것을 허용하지 않을 수 있습니다.

NavigationService 클래스는 일반적으로 테스트 가능성을 높이기 위해 뷰 모델에서 호출됩니다. 그러나 뷰 모델에서 보기로 이동하려면 뷰 모델에서 뷰를 참조해야 하며, 특히 활성 보기 모델이 연결되지 않은 뷰는 권장되지 않습니다. 따라서 NavigationService 여기에 표시된 뷰 모델 형식을 탐색할 대상으로 지정합니다.

eShopOnContainers 모바일 앱은 클래스를 NavigationService 사용하여 모델 우선 보기 탐색을 제공합니다. 이 클래스는 다음 코드 예제에 표시된 INavigationService 인터페이스를 구현합니다.

public interface INavigationService  
{  
    ViewModelBase PreviousPageViewModel { get; }  
    Task InitializeAsync();  
    Task NavigateToAsync<TViewModel>() where TViewModel : ViewModelBase;  
    Task NavigateToAsync<TViewModel>(object parameter) where TViewModel : ViewModelBase;  
    Task RemoveLastFromBackStackAsync();  
    Task RemoveBackStackAsync();  
}

이 인터페이스는 구현 클래스가 다음 메서드를 제공해야 한다고 지정합니다.

메서드 용도
InitializeAsync 앱이 시작될 때 두 페이지 중 하나로 이동합니다.
NavigateToAsync 지정된 페이지에 대한 계층적 탐색을 수행합니다.
NavigateToAsync(parameter) 매개 변수를 전달하여 지정된 페이지에 대한 계층적 탐색을 수행합니다.
RemoveLastFromBackStackAsync 탐색 스택에서 이전 페이지를 제거합니다.
RemoveBackStackAsync 탐색 스택에서 이전 페이지를 모두 제거합니다.

또한 인터페이스는 INavigationService 구현 클래스가 속성을 제공해야 PreviousPageViewModel 한다고 지정합니다. 이 속성은 탐색 스택의 이전 페이지와 연결된 뷰 모델 형식을 반환합니다.

참고 항목

INavigationService 인터페이스는 일반적으로 탐색 스택의 이전 페이지로 프로그래밍 방식으로 돌아가는 데 사용되는 GoBackAsync 메서드도 지정합니다. 그러나 이 메서드는 필수가 아니므로 eShopOnContainers 모바일 앱에서 누락되었습니다.

NavigationService 인스턴스 만들기

인터페이스를 INavigationService 구현하는 클래스는 NavigationService 다음 코드 예제와 같이 Autofac 종속성 주입 컨테이너를 사용하여 싱글톤으로 등록됩니다.

builder.RegisterType<NavigationService>().As<INavigationService>().SingleInstance();

INavigationService 인터페이스는 다음 코드 예제에 ViewModelBase 설명된 대로 클래스 생성자에서 확인됩니다.

NavigationService = ViewModelLocator.Resolve<INavigationService>();

클래스의 메서드에서 NavigationService 만든 Autofac 종속성 주입 컨테이너에 저장된 개체에 대한 참조를 InitNavigationApp 반환합니다. 자세한 내용은 앱이 시작될 때 탐색을 참조하세요.

ViewModelBase 클래스는 NavigationService 인스턴스를 INavigationService 형식의 NavigationService 속성에 저장합니다. 따라서 클래스에서 파생되는 모든 뷰 모델 클래스는 ViewModelBase 이 속성을 사용하여 NavigationService 인터페이스에 지정된 메서드에 INavigationService 액세스할 수 있습니다. 이렇게 하면 Autofac 종속성 주입 컨테이너에서 각 뷰 모델 클래스에 개체를 삽입 NavigationService 하는 오버헤드가 방지됩니다.

탐색 요청 처리

Xamarin.FormsNavigationPage 는 사용자가 원하는 대로 페이지, 앞뒤로 탐색할 수 있는 계층적 탐색 환경을 구현하는 클래스를 제공합니다. 계층적 탐색에 대한 자세한 내용은 계층적 탐색을 참조하세요.

eShopOnContainers 앱은 클래스를 NavigationPage 직접 사용하는 대신 다음 코드 예제와 같이 클래스의 클래스 CustomNavigationView 를 래핑합니다NavigationPage.

public partial class CustomNavigationView : NavigationPage  
{  
    public CustomNavigationView() : base()  
    {  
        InitializeComponent();  
    }  

    public CustomNavigationView(Page root) : base(root)  
    {  
        InitializeComponent();  
    }  
}

이 래핑의 목적은 클래스의 XAML 파일 내에서 인스턴스를 쉽게 스타일링 NavigationPage 하기 위한 것입니다.

탐색은 다음 코드 예제에 설명된 대로 탐색할 페이지의 보기 모델 형식을 지정하여 메서드 중 NavigateToAsync 하나를 호출하여 뷰 모델 클래스 내에서 수행됩니다.

await NavigationService.NavigateToAsync<MainViewModel>();

다음 코드 예제에서는 클래스에서 NavigateToAsync 제공하는 메서드를 보여 줍니다 NavigationService .

public Task NavigateToAsync<TViewModel>() where TViewModel : ViewModelBase  
{  
    return InternalNavigateToAsync(typeof(TViewModel), null);  
}  

public Task NavigateToAsync<TViewModel>(object parameter) where TViewModel : ViewModelBase  
{  
    return InternalNavigateToAsync(typeof(TViewModel), parameter);  
}

각 메서드를 사용하면 클래스에서 ViewModelBase 파생된 뷰 모델 클래스가 메서드를 호출하여 계층적 탐색을 수행할 수 있습니다 InternalNavigateToAsync . 또한 두 번째 NavigateToAsync 메서드를 사용하면 탐색 데이터를 탐색 중인 뷰 모델에 전달되는 인수로 지정할 수 있으며, 이 인수는 일반적으로 초기화를 수행하는 데 사용됩니다. 자세한 내용은 탐색 중에 매개 변수 전달을 참조 하세요.

이 메서드는 InternalNavigateToAsync 탐색 요청을 실행하고 다음 코드 예제에 나와 있습니다.

private async Task InternalNavigateToAsync(Type viewModelType, object parameter)  
{  
    Page page = CreatePage(viewModelType, parameter);  

    if (page is LoginView)  
    {  
        Application.Current.MainPage = new CustomNavigationView(page);  
    }  
    else  
    {  
        var navigationPage = Application.Current.MainPage as CustomNavigationView;  
        if (navigationPage != null)  
        {  
            await navigationPage.PushAsync(page);  
        }  
        else  
        {  
            Application.Current.MainPage = new CustomNavigationView(page);  
        }  
    }  

    await (page.BindingContext as ViewModelBase).InitializeAsync(parameter);  
}  

private Type GetPageTypeForViewModel(Type viewModelType)  
{  
    var viewName = viewModelType.FullName.Replace("Model", string.Empty);  
    var viewModelAssemblyName = viewModelType.GetTypeInfo().Assembly.FullName;  
    var viewAssemblyName = string.Format(  
                CultureInfo.InvariantCulture, "{0}, {1}", viewName, viewModelAssemblyName);  
    var viewType = Type.GetType(viewAssemblyName);  
    return viewType;  
}  

private Page CreatePage(Type viewModelType, object parameter)  
{  
    Type pageType = GetPageTypeForViewModel(viewModelType);  
    if (pageType == null)  
    {  
        throw new Exception($"Cannot locate page type for {viewModelType}");  
    }  

    Page page = Activator.CreateInstance(pageType) as Page;  
    return page;  
}

메서드는 InternalNavigateToAsync 먼저 메서드를 호출하여 뷰 모델에 대한 탐색을 수행합니다 CreatePage . 이 메서드는 지정된 뷰 모델 형식에 해당하는 뷰를 찾고 이 뷰 형식의 인스턴스를 만들고 반환합니다. 뷰 모델 형식에 해당하는 뷰를 찾는 경우 다음과 같이 가정하는 규칙 기반 접근 방식이 사용됩니다.

  • 뷰는 뷰 모델 형식과 동일한 어셈블리에 있습니다.
  • 보기는 .에 있습니다. 자식 네임스페이스를 표시합니다.
  • 보기 모델은 에 있습니다. ViewModels 자식 네임스페이스입니다.
  • 보기 이름은 "모델"이 제거된 모델 이름 보기에 해당합니다.

뷰가 인스턴스화되면 해당 뷰 모델과 연결됩니다. 이러한 상황이 발생하는 방법에 대한 자세한 내용은 뷰 모델 로케이터를 사용하여 뷰 모델 자동 만들기를 참조하세요.

생성되는 뷰가 a LoginView인 경우 클래스의 새 인스턴스 CustomNavigationView 안에 래핑되고 속성에 Application.Current.MainPage 할당됩니다. 그렇지 않으면 인스턴스가 CustomNavigationView 검색되고 null PushAsync 이 아닌 경우 메서드가 호출되어 생성되는 보기를 탐색 스택에 푸시합니다. 그러나 검색된 CustomNavigationView 인스턴스인 null경우 생성되는 뷰는 클래스의 CustomNavigationView 새 인스턴스 내부에 래핑되고 속성에 Application.Current.MainPage 할당됩니다. 이 메커니즘은 탐색 중에 페이지가 비어 있을 때와 데이터가 포함된 경우 모두 탐색 스택에 올바르게 추가되도록 합니다.

페이지를 캐싱하는 것이 좋습니다. 페이지 캐싱을 사용하면 현재 표시되지 않는 보기에 대한 메모리 사용량이 발생합니다. 그러나 페이지 캐싱이 없으면 새 페이지가 탐색될 때마다 페이지 및 뷰 모델의 XAML 구문 분석 및 생성이 발생하므로 복잡한 페이지에 성능에 영향을 줄 수 있습니다. 과도한 수의 컨트롤을 사용하지 않는 잘 디자인된 페이지의 경우 성능이 충분해야 합니다. 그러나 페이지 캐싱은 느린 페이지 로드 시간이 발생하는 경우 도움이 될 수 있습니다.

뷰를 만들고 탐색한 InitializeAsync 후 뷰의 연결된 뷰 모델의 메서드가 실행됩니다. 자세한 내용은 탐색 중에 매개 변수 전달을 참조 하세요.

앱이 시작되면 클래스의 InitNavigation 메서드가 App 호출됩니다. 다음 코드 예제에서는 이 메서드를 보여줍니다.

private Task InitNavigation()  
{  
    var navigationService = ViewModelLocator.Resolve<INavigationService>();  
    return navigationService.InitializeAsync();  
}

메서드는 Autofac 종속성 주입 컨테이너에 새 NavigationService 개체를 만들고 메서드를 호출하기 전에 해당 개체에 대한 참조를 반환합니다 InitializeAsync .

참고 항목

인터페이스가 INavigationService 클래스에 의해 ViewModelBase 확인되면 컨테이너는 InitNavigation 메서드가 호출될 때 생성된 개체에 대한 참조 NavigationService 를 반환합니다.

다음 코드 예제는 NavigationServiceInitializeAsync 메서드를 보여줍니다.

public Task InitializeAsync()  
{  
    if (string.IsNullOrEmpty(Settings.AuthAccessToken))  
        return NavigateToAsync<LoginViewModel>();  
    else  
        return NavigateToAsync<MainViewModel>();  
}

MainView 앱에 인증에 사용되는 캐시된 액세스 토큰이 있는지 확인합니다. 그렇지 않으면 탐색 LoginView 됩니다.

Autofac 종속성 주입 컨테이너에 대한 자세한 내용은 종속성 주입 소개를 참조하세요.

탐색 중 매개 변수 전달

인터페이스에서 NavigateToAsyncINavigationService 지정한 메서드 중 하나를 사용하면 탐색 데이터를 탐색 중인 뷰 모델에 전달되는 인수로 지정할 수 있습니다. 이 인수는 일반적으로 초기화를 수행하는 데 사용됩니다.

예를 들어 ProfileViewModel 클래스에는 사용자가 ProfileView 페이지에서 주문을 선택할 때 실행되는 OrderDetailCommand가 포함됩니다. 그러면 다음 코드 예제에 표시된 OrderDetailAsync 메서드가 실행됩니다.

private async Task OrderDetailAsync(Order order)  
{  
    await NavigationService.NavigateToAsync<OrderDetailViewModel>(order);  
}

이 메서드는 탐색을 호출하여 OrderDetailViewModel사용자가 페이지에서 선택한 순서를 나타내는 인스턴스를 ProfileView 전달 Order 합니다. 클래스가 NavigationService 만들어 OrderDetailView지면 클래스가 OrderDetailViewModel 인스턴스화되고 뷰 BindingContext에 할당됩니다. 탐색한 OrderDetailView후 메서드는 InternalNavigateToAsync 뷰의 연결된 뷰 모델의 메서드를 실행합니다 InitializeAsync .

InitializeAsync 메서드는 클래스에서 재정의 ViewModelBase 할 수 있는 메서드로 정의됩니다. 이 메서드는 object 탐색 작업 중에 뷰 모델에 전달할 데이터를 나타내는 인수를 지정합니다. 따라서 탐색 작업에서 데이터를 수신하려는 뷰 모델 클래스는 필요한 초기화를 수행하는 메서드의 InitializeAsync 고유한 구현을 제공합니다. 다음 코드 예제에서는 OrderDetailViewModel 클래스의 InitializeAsync 메서드를 보여 줍니다.

public override async Task InitializeAsync(object navigationData)  
{  
    if (navigationData is Order)  
    {  
        ...  
        Order = await _ordersService.GetOrderAsync(  
                        Convert.ToInt32(order.OrderNumber), authToken);  
        ...  
    }  
}

이 메서드는 Order 탐색 작업 중에 뷰 모델에 전달된 인스턴스를 검색하고 이를 사용하여 인스턴스에서 OrderService 전체 순서 세부 정보를 검색합니다.

동작을 사용하여 탐색 호출

탐색은 일반적으로 사용자 상호 작용에 의해 뷰에서 트리거됩니다. 예를 들어 LoginView는 인증 성공 후 탐색을 수행합니다. 다음 코드 예제에서는 동작에 의해 탐색이 호출되는 방법을 보여줍니다.

<WebView ...>  
    <WebView.Behaviors>  
        <behaviors:EventToCommandBehavior  
            EventName="Navigating"  
            EventArgsConverter="{StaticResource WebNavigatingEventArgsConverter}"  
            Command="{Binding NavigateCommand}" />  
    </WebView.Behaviors>  
</WebView>

런타임 시 EventToCommandBehaviorWebView와의 상호 작용에 응답합니다. WebView 웹 페이지 Navigating 로 이동하면 이벤트가 발생하며, 이 이벤트가 발생 NavigateCommandLoginViewModel합니다. 이 이벤트는 기본적으로 이벤트에 대한 이벤트 인수는 명령에 전달됩니다. 이 데이터는 WebNavigatingEventArgs에서 Url를 반환하는 EventArgsConverter 속성에 지정된 변환기를 통해 원본과 대상 간에 전달될 때 변환됩니다. 따라서 실행될 때 NavigationCommand 웹 페이지의 URL은 등록된 매개 변수로 전달됩니다 Action.

그러면 NavigationCommand이 다음 코드 예제에 표시된 NavigateAsync 메서드를 실행합니다.

private async Task NavigateAsync(string url)  
{  
    ...          
    await NavigationService.NavigateToAsync<MainViewModel>();  
    await NavigationService.RemoveLastFromBackStackAsync();  
    ...  
}

이 메서드는 탐색을 MainViewModel호출하고 다음 탐색을 수행하면 LoginView 탐색 스택에서 페이지가 제거됩니다.

탐색 확인 또는 취소

사용자가 탐색을 확인 또는 취소할 수 있도록 앱이 탐색 작업 중에 사용자와 상호 작용해야 할 수 있습니다. 예를 들어 사용자가 데이터 입력 페이지를 완전히 작성하기 전에 탐색을 시도하는 경우에 필요할 수 있습니다. 이 경우 앱은 사용자가 페이지를 벗어나도록 허용하거나 탐색 작업이 발생하기 전에 해당 작업을 취소할 수 있는 알림을 제공해야 합니다. 이 작업은 알림의 응답을 사용하여 탐색이 호출되는지 여부를 제어하여 뷰 모델 클래스에서 수행할 수 있습니다.

요약

Xamarin.Forms 에는 내부 논리 기반 상태 변경의 결과로 일반적으로 사용자가 UI와 상호 작용하거나 앱 자체에서 발생하는 페이지 탐색에 대한 지원이 포함됩니다. 그러나 탐색은 MVVM 패턴을 사용하는 앱에서 구현하기 복잡할 수 있습니다.

이 장에서는 NavigationService 뷰 모델에서 모델 우선 보기 탐색을 수행하는 데 사용되는 클래스를 제시했습니다. 보기 모델 클래스에 탐색 논리를 배치하면 자동화된 테스트를 통해 논리를 실행할 수 있습니다. 또한 뷰 모델은 특정 비즈니스 규칙이 적용되도록 탐색을 제어하는 논리를 구현할 수 있습니다.