Build a Todo Example Extension | Mendix Documentation
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. 실행!