본문 바로가기

IT 살이/04. 기술 - 프로그래밍

개발 프레임워크 만들기 대장정 44 - WCF 확장하기 III ( 프로그레스바 자동 보여주기)

■ 프로그레스바 출력 시나리오

 

프로그레스바 자동 출력 기능을 위해서 달봉이는 다음과 같은 시나리오를 정의했다.

프로그레스바를 보여주는 경우를 생각해보자. 이 녀석은 서비스를 호출할때마다 보여줘야 할까? 달봉이는 아니라고 생각하고 달봉이 개발 프레임워크를 구현했다. 예를 들어 업무성 코드 목록을 가져와서 드롭다운 리스트를 채우거나 사용자에게 보여줄 메세지를 가져오는 경우라면 프로그레스바 없이 내부적으로 조용히 처리하면 될 것이다. 달봉이는, 서비스 호출 결과가 늦어질 가능성이 있는 경우에만 프로그레스바를 보여주자고 결정했다.

프로그레스바 보여주는 것과 관련해서 또 한가지 달봉이가 결정한 것은 프로그레스바를 보여줄 정도로 시간이 걸리는 작업은 비동기로 구현하겠다는 것이다. 서비스 호출하고 나서 사용자를 아무 반응도 않는 애플리케이션을 바라보고만 있도록 하는 것은 바람직하지 않는 듯하다. 탭을 선택해서 다른 화면을 볼 수도 없다. 너무 답답한 일이다.

그리고 달봉이는 프로그레스바를 보여주지 않아도 되는 서비스 호출의 경우는 동기를 쓰도록 하겠다. 그러나 강제적으로 개발 프레임워크단에서 제한할 수 있는 방법은 없다. 어떤 방식의 호출을 사용할지는 개발자의 선택에 달려있다. 다만 달봉이가 제공하는 프락시 팩토리 객체는 동기와 비동기 호출을 할 수 있는 방법을 모두 제공해줄 뿐이다. 업무적인 시나리오에 맞게 개발자가 적절한 호출을 선택해야 할 것이다.

그리고 또 한가지 프로그레스바는 업무 화면 객체별로 그 상태를 가지게 될 것이다. 여러 개의 웹 페이지가 각각의 탭으로 열려있는 웹 브라우저를 생각해보자. 하나의 페이지에서 서버에 요청을 보내면 브라우저의 상태바에 프로그레스바가 출력된다. 그러나 다른 페이지의 탭을 선택하면 프로그레스바가 사라지고 현재 선택된 페이지의 진행상태에 따라서 프로그레스바의 출력 여부가 결정된다. 달봉이는 이 시나리오를 염두에 두고 구현을 했다.

 

■ 관련 클래스들

 

프로그레스바를 개발자의 코딩없이 보여줄 수 있도록 구현된 달봉이의 코드를 보도록 하자. 몇 개의 타입이 다시 정의되거나 새롭게 정의된다. 복잡하다.

 ServiceCallNotigyBehavior

Dalbong2.ServiceClient 프로젝트의 ServiceCallNotifyClientEndpointBehavior.cs에 정의되어 있다.

이 녀석이 WCF 런타임에 등록될 behavior이다. 달봉이는 “ServiceCallNotify” behavior라 부르기로 했다. 서비스 호출이 시작되거나 종료될때 위에 정의에 두 이벤트를 발생시켜준다.

 Dalbong2ProxyFactory1

Dalbong2.ServiceClient 프로젝트의 Dalbong2ProxyFactory.cs에 정의되어 있다.

이 녀석은 ServiceCallNotify behavior를 WCF 런타임이 인식할 수 있도록 behavior 목록에 등록시켜준다. 또한 ServiceCallNotify에서 발생한 이벤트를 클라이언트 코드에 전달해주는 역할을 한다. 이를 위해서 Start, End에 해당하는 자신만의 이벤트 멤버를 가지고 있다.

 BongServiceFactory

이 녀석부터 차즘 UI 컨트롤과 접촉을 시도하는 부분이다. 이 녀석이 Bong.Win 프로젝트에 정의되어 있는 이유이다. 우선 이 녀석은 화면 객체에 대한 참조를 받는다. 뒤에서 보겠지만 화면 객체들은 IProgressBarPerceptible 인터페이스를 구현하고 있다. 개발자는 서비스 호출시 프로그레스바를 보여주고 싶다면 서비스 프락시 객체를 생성할때 IProgressBarPerceptible 객체를 받는 프락시 객체 생성 메소드를 이용해야 한다.

public TService GetServiceProxy(string baseAddressKey,

string relativeAddress,

IProgresssBarPerceptible progressBarPerceptibleElement )

세번째 인자로 IProgressBarPerceptible 객체를 넘겨준다. 보통 업무 화면에서 이 메소드를 사용해서 서비스를 호출할때는 this를 넘겨주면 된다. 달봉이의 모든 업무 화면 객체는 IProgressBarPerceptible 인터페이스를 구현하고 있기때문이다.

 IProgressBarPerceptible

BongServiceProxyFactory는 Dalbong2ProxyFactory 객체로부터 서비스 호출 시작/종료에 대한 알림을 받으면 GetServiceProxy() 메소드를 통해서 받은 IProgressBarPerceptible 객체의 속성을 변경시켜준다. 우선 서비스 호출이 시작되었음을 WhileCallingService 속성과 ProgressBarVisibility값을 true로 변경시켜준다.

만약 프로그레스바를 보여주고 싶지 않다면 다음 버전의 프락시 객체 생성 메소드를 사용해야 한다.

public TService GetServiceProxy(string baseAddressKey, string relativeAddress)

업무성 코드나 메세지값을 받아 올때는 두번째 버전을 사용하면 되겠다.

다음은 업무 화면의 베이스 클래스의 정의이다.

 Dalbong2ControlBase

Dalbong2ControlBase 클래스는 그림처럼 IProgressBarPerceptible을 구현하고 있다. 또한 BongServiceProxyFactory가 설정하는 ProgressBarVisibility 속성이 변경되면 ProgressBarVisibleChanged 이벤트가 발생한다.

 

■ 프로그레스바 컨트롤 접근

 

프로그레스바 컨트롤은 업무 화면에서 직접 접근하는 것이 아니다. 단지 업무화면 객체에서는 WhileCallingService 속성과 ProgressBarVisibleChanged 이벤트만을 노출시켜준다. 화면이 전환되거나 또는 서비스 호출이 종료되어 업무 객체의 ProgressBarVisibility 속성이 변경되어 ProgressBarVisibleChanged 이벤트가 발생했을때 그 변화를 감지해서 프로그레스바 컨트롤에 직접 접근해서 그 visible 상태를 변경시켜주는 것은 UI 컨트롤에서 담당한다.

 ProgressBarVisibility

이런 구조로 가면, 사용자 정의 프로그레스바 컨트롤을 사용하더라도 그것을 직접 참조하고 있는 UI 컨테이너만 수정되면 된다. UI 컨테이너가 프로그레스바의 Visible 상태를 변경하기 위해서 그 컨트롤에 직접 직접하는 경우는 두 가지이다.

 

■ 프로그레스바 컨트롤 visible 상태 변경 경우

 

우선 사용자가 화면 탭을 전환해서 현재 보여주는 화면이 변경되는 경우이다. 사용자가 화면 A에서 화면 B로 전환할때 현재는 탭 컨트롤의 SelectionChanged 이벤트 핸들러를 이용하고 있다.

/// <summary>

/// 탭 컨트롤, 탭변경시 작업

/// 1. 프로그레스바 Visibility 변경

/// </summary>

/// <param name="sender"></param>

/// <param name="e"></param>

private void tabControl1_SelectionChanged(object sender, SelectionChangedEventArgs e)

{

    // 프로그레스바 보여주기/숨기기

    TabItem tabItem = (sender as TabControl).SelectedItem as TabItem;

    if (tabItem == null) return;

    //tabItem의 Tag 객체에 저장해둔 업무 화면 객체를 IProgressBarPerceptioble로 변환한다.

    IProgresssBarPerceptible element = tabItem.Tag as IProgresssBarPerceptible;

    if (element != null)

    {

        //현재 업무 화면이 서비스 호출중인지를 확인해서,

        //프로그레스바 컨트롤에 접근해서 visible 상태를 변경한다.

        if( element.WhileCallingService  )

            this.ProgressBar.Visibility = Visibility.Visible;

        else

            this.ProgressBar.Visibility = Visibility.Hidden;

    }

    else

    {

        this.ProgressBar.Visibility = Visibility.Hidden;

    }

}

UI 컨테이너가 프로그레스바 컨트롤의 visible 상태를 변경하기 위해서 직접 접근하는 경우로는 업무 화면 객체가 ProgressBarVisibleChanged 이벤트를 발생시켰을 경우이다.

UI 컨테이너는 이 이벤트를 받기 위해서 업무 화면 객체가 생성해서 탭 컨트롤에 출력할때 그 핸들러를 등록하고 있다. 업무 화면 객체를 Spring.NET 컨테이너에서 가져와서 탭 컨트롤에 추가하는 코드는 이전에 보았다. 이 작업은 메뉴 트리 컨트롤의 MouseUp 이벤트에서 하고 있다. 이 코드는 Shell.cs에 포함되어 있다.

void treeItem_MouseUp(object sender, MouseButtonEventArgs e)

{

    TreeViewItem item = sender as TreeViewItem;

    //Tag 속성에 메뉴 정보 객체 복원

    FileMenuItemInfo menuInfo = item.Tag as FileMenuItemInfo;

    if (menuInfo != null)

    {

        string elemetName = menuInfo.ElementInfo.ID;

 

        IDalbong2Element existingElementInfo = null;

        //이미 같은 화면이 로딩되어 있는지 확인

        //이미 로딩되어 있다면 탭을 생성하지 않고 리턴

        <중략…>

        // Spring.NET 객체 생성기를 통해서 화면 객체를 얻는다.

        Dalbong2ControlBase  uiElement =

            ( Dalbong2ControlBaseBongWinAppContext.XmlObjectFactory.GetObject(elemetName);

       uiElement.ProgressBarVisibleChanged +=

            new ProgressBarVisibleChangedEventHandler(uiElement_ProgressBarVisibleChangedEvent);

 

        <중략…>

 

 

        //탭을 탭 컨트롤에 추가한다.

        this.tabControl1.Items.Add(tabItem);

 

        tabItem.IsSelected = true;

    }

}

UI컨테이너의  uiElement_ProgressBarVisibleChangedEvent 핸들러는 다음과 같이 구현되어 있다.

/// <summary>

/// 화면객체 로딩시, 프로그레스바 Visibility 변경

/// This method checks to see if the current thread needs to be marshalled

/// to the correct (UI owner) thread. If it does a new delegate is created

/// which recalls this method on the correct thread

/// </summary>

/// <param name="sender"></param>

/// <param name="arg"></param>

void uiElement_ProgressBarVisibleChangedEvent(object sender, ProgressBarVisibleChangedEventArgs arg)

{

    if (!this.Dispatcher.CheckAccess())

    {

        this.Dispatcher.BeginInvoke(DispatcherPriority.Normal,

        new ProgressBarVisibleChangedEventHandler(uiElement_ProgressBarVisibleChangedEvent),

        sender, arg);

        return;

    }

 

    // 어떤 화면 객체에서 이벤트를 발생했고,

    // 어떤 이벤트 인자를 넘겼는지를 구한다.

    IDalbong2Element element = sender as IDalbong2Element;

    Visibility visibility = arg.Visibility;

    if (this.CurrentElement != null)

    {

        if (this.CurrentElement.ElementInfo.ID == element.ElementInfo.ID)

        {

            this.ProgressBar.Visibility = visibility;

        }

    }

}

첫번째 이탤릭체로 되어 있는 부분은 다음 포스트에서 비동기 호출을 보면서 다시 보게 될 것이다. 그때 다시 설명하겠다. 지금은 이 이벤트 핸들러가 UI 쓰레드와 다른 쓰레드에서 호출되기때문에 이 이탤릭체 부분의 코드가 필요하다는 것만 언급해두고 넘어가겠다.

볼드체로 되어 있는 부분에서는 먼저, ProgressBarVisibleChangedEvent 이벤트를 발생시킨 업무 화면 객체와 이벤트 인자를 통해서 넘겨준 인자를 구하고 있다.

ProgressBarVisibleChangedEvent 이벤트는 업무 화면 객체 Dalbong2ControlBase( 그리고 Dalbong2PageBase)에 구현되어 있다.

public class Dalbong2ControlBase : UserControl, IDalbong2Element, IProgresssBarPerceptible

{

    #region IDalbong2Element Members

    <중략>…

    #endregion

 

    #region IProgresssBarPerceptible Members

    public event ProgressBarVisibleChangedEventHandler ProgressBarVisibleChanged;

 

    private bool _CallingService = false;

    public bool WhileCallingService

    {

        get

        {

            return _CallingService;

        }

        set

        {

            _CallingService = value;

        }

    }

 

    private Visibility _ProgressBarVisibility = Visibility.Collapsed;

    public Visibility ProgressBarVisibility

    {

        get

        {

            return _ProgressBarVisibility;

        }

        set

        {

            if (_ProgressBarVisibility != value)

            {

                _ProgressBarVisibility = value;

                ProgressBarVisibleChangedEventArgs arg =

                    new ProgressBarVisibleChangedEventArgs(_ProgressBarVisibility);

                OnProgressBarVisibleChanged(arg);

            }

        }

    }

    #endregion

 

    protected virtual void OnProgressBarVisibleChanged(ProgressBarVisibleChangedEventArgs arg)

    {

        if (ProgressBarVisibleChanged != null)

        {

            ProgressBarVisibleChanged(this, arg);

        }

    }

}

ProgressVarVisibility 속성의 값을 설정할때 현재값과 다른 값이 들어오면 ProgressBarVisibleChanged 이벤트를 발생시킨다. 앞의 그림에서 본 것처럼 ProgressVarVisibility 속성은 BongServiceProxyFactory에서 설정한다.

ProgressBarVisibleChangedEvent 이벤트 발생시 추가적인 정보를 전달하기 위해서 다음과 같은 이벤트 인자를 정의하고 있다.

public delegate void ProgressBarVisibleChangedEventHandler( object sender,

ProgressBarVisibleChangedEventArgs arg );

public class ProgressBarVisibleChangedEventArgs

{

 

    private Visibility _Visibility = Visibility.Collapsed;

 

    public ProgressBarVisibleChangedEventArgs(Visibility visibility)

    {

        _Visibility = visibility;

    }

 

    public Visibility Visibility

    {

        get

        {

            return _Visibility;

        }

    }

}

이 이벤트 인자는 ProgressBarVisibleChangedEvent 발생시 현재 visible 상태를 전달하고 있다.

UI 컨테이너에서는 ProgressBarVisibleChangedEvent 이벤트를 발생시킨 업무 화면 객체가 현재 활성화되어 있는 업무 객체와 같은지를 우선 체크하고 그리고 이벤트 인자를 통해서 넘겨준 visible 상태를 통해서 프로그레스바 활성화를 결정하게 되는 것이다.

이것으로 달봉이가 구현해놓은 프로그레스바 자동 출력하기 구조 설명은 끝났다. 좀 복잡한 듯하다.

 

■ Dalbong2ProxyFactory vs. BongServiceProxyFactory

 

마지막으로 하나 더 언급할 것은 Dalbong2ProxyFactory와 BongServiceProxyFactory의 차이점이다.

첫번째로 Dalbong2ProxyFactory는 단지 서비스 시작과 종료를 알리는 이벤트만을 발생시킬 뿐이다. Dalbong2ProxyFactory에게는 UI단에서 어떤 컨트롤을 사용하고 어떤 애플리케이션에서 이 이벤트를 사용하는지는 중요하지 않다. 그래서 Dalbong2ProxyFactory를 다음처럼 Dalobg2.Win 프로젝트에 포함된 것이 아니라 Dalbong2.ServiceClient 프로젝트에 포함되어 있다.

 Dalbong2ProxyFactory

그러나 BongServiceProxyFactory는 서서히 UI단과 관계를 갖기 시작한다. 서비스 프락시 객체를 생성할때  IProgresssBarPerceptible 인자를 받는데 이 녀석은 업무 화면 객체가 구현하고 있는 인터페이스이다. 따라서 BongServiceProxyFactory는 Bong.Win 프로젝트에 구현되어 있다.

  BongServiceProxyFactory

프로그레스바의 자동 출력을 원하는 개발자는 BongServiceProxyFactory 객체를 사용해서 프락시 객체를 이용해야 하는 것이다.

 

■ 실행결과

 

이제 이 기능을 이용해서 구현된 프로그레스바 출력 기능이 어떻게 나타나는지를 보자.

 화면1

 화면2

업무 화면 1과 업무 화면 2는 같은 서버측 메소드를 호출하고 있다. 각각의 버튼을 클릭해서 서비스를 호출해서 서버측에서 서비스를 처리중이더라도 탭 전환이 가능하다. 또한 탭 전환이 이뤄질때 각 화면의 상태에 따라서 상태바에 있는 프로그레스바의 출력 여부가 결정된다.

이것으로 끝이다.

그럼 다음 포스트에서는 BongServiceProxyFactory 객체를 이용해서 서비스를 호출하는 코드를 작성해본다.