NGMsoftware

NGMsoftware
로그인 회원가입
  • 매뉴얼
  • 학습
  • 매뉴얼

    학습


    C# C# .NET 매크로 프로그램 만들기. (스크립트와 모델의 사용 여부 동기화 방법)

    페이지 정보

    본문

    안녕하세요. 엔지엠소프트웨어입니다. 오늘은 스크립트와 모델의 사용 여부 속성을 동기화하는 방법인데요. 좀 더 정확히 말하면 스크립트 편집기의 트리뷰(TreeView)와 모델의 사용 여부 속성을 연결하는 방법입니다. 사용 여부는 아래 그림과 같이 액션 앞에 붙어 있는 체크박스를 말합니다.

    sZbwJrX.png

     

     

    우선, 스크립트 뷰에서 체크박스를 사용하지 않음으로 해제하면 우측의 속성창에 사용 여부도 값이 변경되어야 합니다. 동기화 하기전에 엔지엠 매크로에서 제공하는 모든 액션의 배이스가 되는 BaseModel에 아래 속성을 하나 추가해줍니다.

    [LocalizedCategory("DefaultAction")]
    [LocalizedDisplayName("IsUse")]
    [LocalizedDescription("IsUse")]
    [Browsable(true)]
    [DefaultValue(true)]
    public bool IsUse { get; set; }

     

    체크박스를 변경해도 모델의 속성이 자동으로 변경되지는 않습니다. 클라이언트의 정보를 액션 모델에 전달하기 위해 TreeView에 AfterCheck 이벤트 핸들러를 만들어줍니다.

    treeView.AfterCheck += TreeView_AfterCheck;

     

    핸들러는 아래와 같이 코딩해야 합니다.

    if (e.Node.Tag is ActionModel)
        ((ActionModel)e.Node.Tag).IsUse = e.Node.Checked;

     

    TreeView의 Node는 Tag에 모델을 저장하고 있는데요. 이 모델이 BaseModel인 ActionModel이라면 IsUse 속성을 변경해줍니다. 여기까지 스크립트 뷰에서 모델의 속성을 업데이트하는 방법을 알아봤습니다. 아직은 단방향만 동작하는데요. 이번에는 모델의 속성이 변경될 때 스크립트 뷰의 TreeNode 체크박스가 변경되도록 해야 합니다. IsUse 속성의 set에서 클라이언트를 찾아와서 현재 TreeNode를 찾습니다.

    var form = System.Windows.Forms.Application.OpenForms.Cast<Form>().FirstOrDefault();
    if (form != null && form is Ai.Interface.IClient)
    {
        var selectNode = ((Ai.Interface.IClient)form).CurrentScript?.TreeView?.SelectedNode;

     

    찾은 트리노드의 체크 속성을 아래와 같이 변경해주면 양방향 데이터 교류가 완성됩니다.

    selectNode.Checked = _isUse;
    var nodes = Ai.Common.Helper.GetNodeList(selectNode.Nodes);
    foreach (var node in nodes)
    {
        node.Checked = _isUse;
        ((ActionModel)node.Tag).IsUse = _isUse;
    }

     

    이제는 스크립트 뷰에서 체크박스를 변경하거나 속성창에서 사용 여부를 변경해도 서로 연결된 것처럼 동작합니다.

     

     

    여기까지 개발하면 다행이겠지만... 이제 큰 문제를 하나 해결해야 합니다. 자기 자신은 사용 여부를 업데이트할 수 있지만, 자신이 가진 하위 액션들은 어떻게 처리가 되어야 할지 고민해봐야 합니다. 일반적으로 부모가 사용되지 않으면 하위 자식들도 모두 사용하지 않아야 합니다. 아래와 같이 하이어라키(Hierarchy: 계층구조)를 가지는 경우 중간에 체크가 풀려 있는건 뭔가 부자연스럽습니다. 상하위 액션들은 서로 관계를 맺고 있기 때문입니다.

    uKnXcH2.png

     

     

    위와 같은 문제를 해결하기 위해서 체크하거나 체크 해제하는 액션의 하위 액션들을 모두 찾아서 IsUse 속성을 변경해야 합니다. 이런 경우에는 재귀함수를 만들어서 하위의 모든 액션들을 찾아내야 합니다. 재귀함수는 어느정도 코드가 정형화되어 있습니다. 구글링을 조금만 해봐도 많은 정보를 얻을 수 있을거에요. 아래 코드는 제가 사용하는 재귀함수인데요. TreeNode의 자식을 넘겨주면 모든 자식 노드를 찾아서 반환해주는 메소드입니다.

    public static TreeNode[] GetNodeList(TreeNodeCollection treeNodes)
    {
        List<TreeNode> nodes = new List<TreeNode>();
    
        foreach (TreeNode node in treeNodes)
        {
            nodes.Add(node);
    
            if (node.Nodes.Count > 0)
                GetNodeList(nodes, node);
        }
    
        return nodes.ToArray();
    }
    
    private static void GetNodeList(List<TreeNode> nodes, TreeNode node)
    {
        foreach (TreeNode n in node.Nodes)
        {
            nodes.Add(n);
    
            if (n.Nodes.Count > 0)
                GetNodeList(nodes, n);
        }
    }

     

    이제 모든 자식 노드를 찾았다면 각각 하나씩 반복하면서 IsUse 속성을 Checked 속성 값으로 업데이트하면 됩니다.

    selectNode.Checked = _isUse;
    var nodes = Ai.Common.Helper.GetNodeList(selectNode.Nodes);
    foreach (var node in nodes)
    {
        node.Checked = _isUse;
        ((ActionModel)node.Tag).IsUse = _isUse;
    }

     

    이렇게 코드를 작성하고 실행하면 프로그램이 죽는 현상이 발생합니다. 왜 그럴까요? 우리는 앞에서 AfterCheck 이벤트 핸들러를 할당했습니다. 이 이벤트 핸들러는 모델의 속성을 업데이트하는데요. 이 때 모델은 트리뷰의 체크박스를 업데이트하므로 이벤트가 다시 발생합니다. 그러면 또다시 IsUse 속성을 업데이트하는 무한루프에 빠지게 됩니다. 모델이 트리뷰를 업데이트하면, 트리뷰는 모델을 업데이트하고 또다시 트리뷰를 업데이트하는게 반복됩니다. 그러면, 작업이 종료되지 않습니다.

     

    이런 상황이 그리 흔한건 아닙니다. 제가 개발자로 20년 넘게 생활하면서 한두번 볼까말까한 에러였거든요. 그리고, 윈도우 앱에서 이런 시나리오를 가진 프로그램이 많지는 않을겁니다. 대부분 웹 환경일테고, 하이어라키 구조까지 가지진 않을거거든요. 일반적인 게시판을 생각해봐도 댓글에 댓글인 대댓글까지 자동으로 처리해주는 기능을 가진곳도 없습니다. 대부분은 1차원적인 UX만을 제공하니까요.

     

    아무튼, 우리는 이 문제를 어떻게든 해결해야 합니다. 어떤 아이디어가 있을지를 생각해봐야 하는데요. 가장 쉽게 생각해볼 수 있는건 단방향으로만 이벤트가 전달되도록 하는겁니다. 그래서, 스크립트 뷰의 이벤트 핸들러에서 AfterCheck 이벤트를 제거하고, 작업이 완료되면 다시 이벤트 핸들러를 연결해주는겁니다. 아래와 같이 코드를 변경해야겠죠?

    private void TreeView_AfterCheck(object? sender, TreeViewEventArgs e)
    {
        if (e.Node != null)
        {
            treeView.AfterCheck -= TreeView_AfterCheck;
    
            if (e.Node.Tag is ActionModel)
                ((ActionModel)e.Node.Tag).IsUse = e.Node.Checked;
    
            treeView.SelectedNode = e.Node;
            treeView.AfterCheck += TreeView_AfterCheck;
        }
    
        if (_client is IEditor)
            ((IEditor)_client).Property.Refresh();
    }

     

    이제 IsUse 속성을 업데이트할 때 이벤트를 제거했기 때문에 문제가 되지는 않을겁니다. 작업이 완료된 후 마지막에 TreeView_AfterCheck 이벤트를 다시 할당하여 원래 동작을 수행할 수 있도록 복원했습니다. 간단한 아이디어지만 무한루프에 빠지는 시나리오를 접할 기회가 없다면 문제 원인을 찾는것도 쉽지 않을겁니다.

     

    매크로를 실행하고, 하나씩 체크하고 해제 해보면 아래 동영상처럼 하이어라키 구조에서도 상하관계에 따라 체크되고 체크 해제됩니다. 참고로, 자신의 부모가 사용되지 않음에도 불구하고 자식 노드를 사용함으로 체크하려고 하면 체크가 되지 않습니다. 만약, 부모가 사용하지 않음인데도 자식들이 전부 사용함으로 된다면 다양한 문제들이 발생할 수 있습니다. 그래서, 상호 참조나 상호 호출과 같이 관계를 맺는 시나리오에서는 규칙을 만들고 정확하게 이행해야 나중에 로직이 꼬이는 문제를 방지할 수 있습니다.

     

     

    지금까지 엔지엠 매크로 6에서 문제가 되었던 내용들을 정리하고 새로운 버전에서는 좀 더 규칙을 철저하게 지켜서 문제가 발생할 수 있는 시나리오를 방지하도록 했습니다. 예상하지 못한 시나리오가 더 존재할수도 있지만, 현재와 같은 규칙을 잘 따르면 구조적인 문제들은 대부분 해소할 수 있을겁니다. 아래 동영상은 하이어라키를 변경해가면서 동작시켜본 테스트입니다.

     

     

    개발자에게 후원하기

    MGtdv7r.png

     

    추천, 구독, 홍보 꼭~ 부탁드립니다.

    여러분의 후원이 빠른 귀농을 가능하게 해줍니다~ 답답한 도시를 벗어나 귀농하고 싶은 개발자~

    감사합니다~

     

    • 네이버 공유하기
    • 페이스북 공유하기
    • 트위터 공유하기
    • 카카오스토리 공유하기
    추천0 비추천0

    댓글목록

    등록된 댓글이 없습니다.