본문 바로가기
IT/c#

Todo Extension 만들기

by 가능성1g 2025. 1. 6.
반응형

Build a Todo Example Extension | Mendix Documentation

 

Build a Todo Example Extension

Introduction This document describes how to build an example extension that adds a simple todo list extension to Studio Pro. With this example extension, you can add new todo items to a list. The example extension will be added to the main menu of Studio P

docs.mendix.com

 

1. 프로젝트 생성

 

2. Nuget 패키지 관리자를 통해 MendixExtensionsAPI 설치

메뉴이동 : 도구 > NuGet 패키지 관리자 > 솔루션용 NuGet 패키지 관리

Mendix ExtensionsAPI 검색

버전에 맞게 설치 ( 10.16.1 )

 

3. manifest.json 파일 추가

manifest.json 파일을 추가하고, 다음 내용을 씀

{
  "mx_extensions": [ "KJB.ToDoExtension.dll" ],
  "mx_build_extensions": []
}

 

manifest.json 파일을 오른쪽 클릭해 속성을 선택하고,  고급 > 출력 디렉토리로 복사 를 "항상 복사" 로 변경

 

3.ToDoListDockablePaneExtension.cs 파일 생성

using Mendix.StudioPro.ExtensionsAPI.Services;
using Mendix.StudioPro.ExtensionsAPI.UI.DockablePane;
using System.ComponentModel.Composition;

namespace KJB.TodoExtension;

[Export(typeof(DockablePaneExtension))]
public class ToDoListDockablePaneExtension : DockablePaneExtension
{
    private readonly ILogService _logService;
    public const string PaneId = "ToDoList";

    [ImportingConstructor]
    public ToDoListDockablePaneExtension(ILogService logService)
    {
        _logService = logService;
    }

    public override string Id => PaneId;

    public override DockablePaneViewModelBase Open()
    {
        return new ToDoListDockablePaneViewModel(WebServerBaseUrl, () => CurrentApp, _logService) { Title = "To Do List" };
    }
}

 

필요할 경우 using 문 정리 Ctrl+R, Ctrl+G

 

4. ToDoListDockablePaneViewModel.cs 파일 생성

using Mendix.StudioPro.ExtensionsAPI.Model;
using Mendix.StudioPro.ExtensionsAPI.Services;
using Mendix.StudioPro.ExtensionsAPI.UI.DockablePane;
using Mendix.StudioPro.ExtensionsAPI.UI.WebView;

namespace KJB.TodoExtension;

public class ToDoListDockablePaneViewModel : WebViewDockablePaneViewModel
{

    private readonly Uri _baseUri;
    private readonly Func<IModel?> _getCurrentApp;
    private readonly ILogService _logService;

    public ToDoListDockablePaneViewModel(Uri baseUri, Func<IModel?> getCurrentApp, ILogService logService)
    {
        _baseUri = baseUri;
        _getCurrentApp = getCurrentApp;
        _logService = logService;
    }

    public override void InitWebView(IWebView webView)
    {
        webView.Address = new Uri(_baseUri, "index");

        webView.MessageReceived += (_, args) =>
        {
            var currentApp = _getCurrentApp();
            if (currentApp == null) return;

            if (args.Message == "AddToDo")
            {
                var toDoText = args.Data["toDoText"]?.GetValue<string>() ?? "New To Do";
                AddToDo(currentApp, toDoText);
                webView.PostMessage("RefreshToDos");
            }

            if (args.Message == "ChangeToDoStatus")
            {
                var toDoId = args.Data["id"]!.GetValue<string>();
                var newIsDone = args.Data["isDone"]!.GetValue<bool>();

                ChangeToDoStatus(currentApp, toDoId, newIsDone);
                webView.PostMessage("RefreshToDos");
            }

            if (args.Message == "ClearDone")
            {
                ClearDone(currentApp);
                webView.PostMessage("RefreshToDos");
            }
        };
    }

    private void AddToDo(IModel currentApp, string toDoText)
    {
        var toDoStorage = new ToDoStorage(currentApp, _logService);
        var toDoList = toDoStorage.LoadToDoList();
        toDoList.ToDos.Add(new ToDoModel(toDoText, false));
        toDoStorage.SaveToDoList(toDoList);
    }

    private void ChangeToDoStatus(IModel currentApp, string toDoId, bool newIsDone)
    {
        var toDoStorage = new ToDoStorage(currentApp, _logService);
        var toDoList = toDoStorage.LoadToDoList();
        var toDo = toDoList.ToDos.FirstOrDefault(x => x.Id == toDoId);
        if (toDo != null)
        {
            toDo.IsDone = newIsDone;
            toDoStorage.SaveToDoList(toDoList);
        }
    }

    private void ClearDone(IModel currentApp)
    {
        var toDoStorage = new ToDoStorage(currentApp, _logService);
        var toDoList = toDoStorage.LoadToDoList();
        toDoList.ToDos.RemoveAll(x => x.IsDone);
        toDoStorage.SaveToDoList(toDoList);
    }
}

 

5. ToDoListModel.cs, ToDoModel.cs, ToDoStorage.cs 생성. 저장관련 소스들.

using System.Text.Json.Serialization;

namespace KJB.TodoExtension;

public record ToDoListModel
{
    [JsonConstructor]
    public ToDoListModel(List<ToDoModel> toDos)
    {
        ToDos = toDos;
    }

    public List<ToDoModel> ToDos { get; }
}
using System.Text.Json.Serialization;

namespace KJB.TodoExtension;

public record ToDoModel
{
    [JsonConstructor]
    public ToDoModel(string id, string text, bool isDone)
    {
        Id = id;
        Text = text;
        IsDone = isDone;
    }

    public ToDoModel(string text, bool isDone)
        : this(Guid.NewGuid().ToString(), text, isDone)
    {
    }

    public string Id { get; set; }
    public string Text { get; set; }
    public bool IsDone { get; set; }
}
using Mendix.StudioPro.ExtensionsAPI.Model;
using Mendix.StudioPro.ExtensionsAPI.Services;
using System.Text;
using System.Text.Json;

namespace KJB.TodoExtension;

public class ToDoStorage
{
    private readonly ILogService _logService;
    private readonly string _toDoFilePath;

    public ToDoStorage(IModel currentApp, ILogService logService)
    {
        _logService = logService;
        _toDoFilePath = Path.Join(currentApp.Root.DirectoryPath, "to-do-list.json");
    }

    public ToDoListModel LoadToDoList()
    {
        ToDoListModel? toDoList = null;

        try
        {
            toDoList = JsonSerializer.Deserialize<ToDoListModel>(File.ReadAllText(_toDoFilePath, Encoding.UTF8));
        }
        catch (Exception exception)
        {
            _logService.Error($"Error while loading To Dos from {_toDoFilePath}", exception);
        }

        return toDoList ?? new ToDoListModel(new[]
        {
            new ToDoModel("Mendix 게시판 만들기", false),
            new ToDoModel("결재 받기", false),
            new ToDoModel("점심먹기", true)
        }.ToList());
    }

    public void SaveToDoList(ToDoListModel toDoList)
    {
        var jsonText = JsonSerializer.Serialize(toDoList, new JsonSerializerOptions() { WriteIndented = true });
        File.WriteAllText(_toDoFilePath, jsonText, Encoding.UTF8);
    }
}

 

6. ToDoListMenuExtension.cs  생성 -> 메뉴를 추가 해주는 파일

using Mendix.StudioPro.ExtensionsAPI.UI.Menu;
using Mendix.StudioPro.ExtensionsAPI.UI.Services;
using System.ComponentModel.Composition;

namespace KJB.TodoExtension;

[Export(typeof(Mendix.StudioPro.ExtensionsAPI.UI.Menu.MenuExtension))]
public class ToDoListMenuBarExtension : MenuExtension
{
    private readonly IDockingWindowService _dockingWindowService;

    [ImportingConstructor]
    public ToDoListMenuBarExtension(IDockingWindowService dockingWindowService)
    {
        _dockingWindowService = dockingWindowService;
    }

    public override IEnumerable<MenuViewModel> GetMenus()
    {
        yield return new MenuViewModel("To Do List", () => _dockingWindowService.OpenPane(ToDoListDockablePaneExtension.PaneId));
    }
}

 

7. 웹 인터페이스를 위해 wwwroot 폴더를 생성하고, index.html, main.js 파일을 추가합니다.

<html lang="ko">
<head>
    <title>To Do List</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <style type="text/tailwindcss">
        @tailwind base;
        @tailwind components;
        @tailwind utilities;

        @layer base {
            body {
                @apply m-6;
            }

            h1 {
                @apply text-2xl mt-6;
            }

            h2 {
                @apply text-xl mt-6;
            }

            input[type=checkbox] + label {
                @apply ml-2;
            }

            input[type=checkbox]:checked + label {
                text-decoration: line-through;
            }

            button {
                @apply text-blue-600 italic
            }
        }
    </style>
</head>
<body>
    <div><label for="addToDoInput">Add to do:</label> <input id="addToDoInput" type="text" placeholder="To do text" /> <button id="addToDoButton">Add</button></div>
    <h1>To Do</h1>
    <div id="todo"></div>
    <h1>Done</h1>
    <div id="done"></div>
    <button id="clearDoneButton">Clear</button>
    <script type="module" src="./main.js"></script>
</body>
</html>
function postMessage(message, data) {
    window.chrome.webview.postMessage({ message, data });
}

// Register message handler.
window.chrome.webview.addEventListener("message", handleMessage);
// Indicate that you are ready to receive messages.
postMessage("MessageListenerRegistered");

async function handleMessage(event) {
    const { message, data } = event.data;
    if (message === "RefreshToDos") {
        await refreshToDos();
    }
}

async function refreshToDos() {
    let todosResponse = await fetch("./todos");
    let todos = await todosResponse.json();

    let todoDiv = document.getElementById("todo");
    let doneDiv = document.getElementById("done");

    let todoItems = [];
    let doneItems = [];

    for (const todo of todos.ToDos) {
        let item = document.createElement("div");

        let checkbox = document.createElement("input");
        checkbox.type = "checkbox";
        checkbox.id = `todo-${todo.Id}`;
        checkbox.checked = todo.IsDone;
        checkbox.addEventListener("click", () => {
            postMessage("ChangeToDoStatus", { id: todo.Id, isDone: !todo.IsDone });
        });

        let label = document.createElement("label");
        label.htmlFor = checkbox.id;
        label.innerText = todo.Text;

        item.replaceChildren(checkbox, label);

        if (todo.IsDone) {
            doneItems.push(item);
        } else {
            todoItems.push(item);
        }
    }

    todoDiv.replaceChildren(...todoItems);
    doneDiv.replaceChildren(...doneItems);
}

async function addToDo() {
    let addToDoInput = document.getElementById("addToDoInput");
    const toDoText = addToDoInput.value;
    postMessage("AddToDo", { toDoText });
    addToDoInput.value = "";
}

document.getElementById("addToDoButton").addEventListener("click", addToDo);
document.getElementById("clearDoneButton").addEventListener("click", () => {
    postMessage("ClearDone");
});

await refreshToDos();

두개 파일을 오른쪽 클릭해 속성을 선택하고,  고급 > 출력 디렉토리로 복사 를 "항상 복사" 로 변경

 

8. 서버가 있어야 위의 2개파일을 기동이 가능해서 WebServerExtension 을 이용한다.

ToDoListWebServerExtension.cs 파일 생성

using Mendix.StudioPro.ExtensionsAPI.Services;
using Mendix.StudioPro.ExtensionsAPI.UI.WebServer;
using System.ComponentModel.Composition;
using System.Net;
using System.Text.Json;

namespace KJB.TodoExtension;

[Export(typeof(WebServerExtension))]
public class ToDoListWebServerExtension : WebServerExtension
{
    private readonly IExtensionFileService _extensionFileService;
    private readonly ILogService _logService;

    [ImportingConstructor]
    public ToDoListWebServerExtension(IExtensionFileService extensionFileService, ILogService logService)
    {
        _extensionFileService = extensionFileService;
        _logService = logService;
    }

    public override void InitializeWebServer(IWebServer webServer)
    {
        webServer.AddRoute("index", ServeIndex);
        webServer.AddRoute("main.js", ServeMainJs);
        webServer.AddRoute("todos", ServeToDos);
    }

    private async Task ServeIndex(HttpListenerRequest request, HttpListenerResponse response, CancellationToken ct)
    {
        var indexFilePath = _extensionFileService.ResolvePath("wwwroot", "index.html");
        await response.SendFileAndClose("text/html", indexFilePath, ct);
    }

    private async Task ServeMainJs(HttpListenerRequest request, HttpListenerResponse response, CancellationToken ct)
    {
        var indexFilePath = _extensionFileService.ResolvePath("wwwroot", "main.js");
        await response.SendFileAndClose("text/javascript", indexFilePath, ct);
    }

    private async Task ServeToDos(HttpListenerRequest request, HttpListenerResponse response, CancellationToken ct)
    {
        if (CurrentApp == null)
        {
            response.SendNoBodyAndClose(404);
            return;
        }

        var toDoList = new ToDoStorage(CurrentApp, _logService).LoadToDoList();
        var jsonStream = new MemoryStream();
        await JsonSerializer.SerializeAsync(jsonStream, toDoList, cancellationToken: ct);

        response.SendJsonAndClose(jsonStream);
    }
}

 

HttpListenerResponseUtils.cs 파일 생성

using System.Net;
using System.Text;

namespace KJB.TodoExtension;

public static class HttpListenerResponseUtils
{
    public static async Task SendFileAndClose(this HttpListenerResponse response, string contentType, string filePath, CancellationToken ct)
    {
        response.AddDefaultHeaders(200);

        var fileContents = await File.ReadAllBytesAsync(filePath, ct);

        response.ContentType = contentType;
        response.ContentLength64 = fileContents.Length;

        await response.OutputStream.WriteAsync(fileContents, ct);

        response.Close();
    }

    public static void SendJsonAndClose(this HttpListenerResponse response, MemoryStream jsonStream)
    {
        response.AddDefaultHeaders(200);

        response.ContentType = "application/json";
        response.ContentEncoding = Encoding.UTF8;
        response.ContentLength64 = jsonStream.Length;

        jsonStream.WriteTo(response.OutputStream);

        response.Close();
    }

    public static void SendNoBodyAndClose(this HttpListenerResponse response, int statusCode)
    {
        response.AddDefaultHeaders(statusCode);

        response.Close();
    }

    static void AddDefaultHeaders(this HttpListenerResponse response, int statusCode)
    {
        response.StatusCode = statusCode;

        // Makes sure the web-code can receive responses
        response.AddHeader("Access-Control-Allow-Origin", "*");
    }
}

 

2개의 파일로 단순한 서버가 생성되어, request/response 가 되는 웹서버에 올리는 개념이다. 

저장은 파일로 된다.

 

9. 적당한 Mendix 프로젝트를 만들고 빌드한 파일을 복사한다. 

참고로 나의 위치 : C:\MENDIX_DEV\ExCreateMenu\extensions\KJB_TodoList

프로젝트 속성에서 빌드 이벤트 빌드 후, 정의를 추가해서 빌드가 되면 자동으로 복사 되게 한다.

 

xcopy /y /s /i "$(TargetDir)" "C:\MENDIX_DEV\ExCreateMenu\extensions\KJB_TodoList"

 

10. 실행!

 

반응형