Last active
September 3, 2025 05:26
-
-
Save engalar/536df8e04cd5574946a62414abb12ad8 to your computer and use it in GitHub Desktop.
studio pro plugin for visualizer untyped model
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- gh gist edit 536df8e04cd5574946a62414abb12ad8 .\index.html -f index.html --> | |
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<title>Mendix Model Browser</title> | |
<script src="assets/react.development.js"></script> | |
<script src="assets/react-dom.development.js"></script> | |
<script src="assets/tailwindcss.js"></script> | |
<script src="assets/babel.min.js"></script> | |
<script src="assets/vconsole.min.js"></script> | |
<script>new VConsole();</script> | |
<style> | |
.copy-icon { | |
opacity: 0.2; | |
transition: opacity 0.2s; | |
cursor: pointer; | |
} | |
.group-hover:hover .copy-icon, | |
.copy-icon:hover { | |
opacity: 1; | |
} | |
.tooltip { | |
position: relative; | |
display: inline-block; | |
} | |
.tooltip .tooltiptext { | |
visibility: hidden; | |
width: 120px; | |
background-color: #555; | |
color: #fff; | |
text-align: center; | |
border-radius: 6px; | |
padding: 5px 0; | |
position: absolute; | |
z-index: 1; | |
bottom: 125%; | |
left: 50%; | |
margin-left: -60px; | |
opacity: 0; | |
transition: opacity 0.3s; | |
} | |
.tooltip .tooltiptext::after { | |
content: ""; | |
position: absolute; | |
top: 100%; | |
left: 50%; | |
margin-left: -5px; | |
border-width: 5px; | |
border-style: solid; | |
border-color: #555 transparent transparent transparent; | |
} | |
.tooltip:hover .tooltiptext { | |
visibility: visible; | |
opacity: 1; | |
} | |
</style> | |
</head> | |
<body class="bg-gray-100 font-sans text-sm"> | |
<div id="app"></div> | |
<script type="text/babel"> | |
const { useState, useEffect, useCallback, useMemo } = React; | |
// --- 1. RPC Client --- | |
class MendixRpcClient { | |
constructor() { | |
this.requestId = 0; | |
this.pendingRequests = new Map(); | |
window.addEventListener('message', (event) => { | |
if (event.data.type === 'backendResponse') this.handleResponse(event.data.data); | |
}); | |
} | |
handleResponse(response) { | |
const data = typeof response === 'string' ? JSON.parse(response) : response; | |
const pendingRequest = this.pendingRequests.get(data.requestId); | |
if (pendingRequest) { | |
this.pendingRequests.delete(data.requestId); | |
if (data.error) pendingRequest.reject(new Error(data.error.message || data.error)); | |
else pendingRequest.resolve(data.result); | |
} | |
} | |
call(method, params = {}) { | |
const requestId = ++this.requestId; | |
return new Promise((resolve, reject) => { | |
this.pendingRequests.set(requestId, { resolve, reject }); | |
window.parent.sendMessage("frontend:message", { jsonrpc: "2.0", method, params, id: requestId }); | |
}); | |
} | |
getNodeChildren(nodeId) { return this.call('getNodeChildren', { nodeId }); } | |
getNodeDetails(nodeId) { return this.call('getNodeDetails', { nodeId }); } | |
getPropertyChildren(propertyId) { return this.call('getPropertyChildren', { propertyId }); } | |
} | |
const rpcClient = new MendixRpcClient(); | |
// --- Helper function for copying to clipboard --- | |
function copyToClipboard(text, tooltipRef) { | |
navigator.clipboard.writeText(text).then(() => { | |
if (tooltipRef && tooltipRef.current) { | |
const originalText = tooltipRef.current.innerText; | |
tooltipRef.current.innerText = 'Copied!'; | |
setTimeout(() => { | |
tooltipRef.current.innerText = originalText; | |
}, 1500); | |
} | |
}).catch(err => { | |
console.error('Failed to copy: ', err); | |
alert('Failed to copy text to clipboard.'); | |
}); | |
} | |
// --- 2. UI Components --- | |
function CopyButton({ textToCopy, tooltipText = "Copy path" }) { | |
const tooltipRef = React.useRef(null); | |
return ( | |
<div className="tooltip" onClick={(e) => { e.stopPropagation(); copyToClipboard(textToCopy, tooltipRef); }}> | |
<span className="copy-icon ml-2"> | |
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg> | |
</span> | |
<span className="tooltiptext" ref={tooltipRef}>{tooltipText}</span> | |
</div> | |
); | |
} | |
function CollapsibleSection({ title, count, children, defaultOpen = false, path }) { | |
const [isOpen, setIsOpen] = useState(defaultOpen); | |
return ( | |
<div className="mb-4"> | |
<h3 onClick={() => setIsOpen(!isOpen)} className="group-hover font-semibold text-gray-700 border-b pb-1 mb-2 cursor-pointer flex justify-between items-center select-none"> | |
<span className="flex items-center"> | |
{title} ({count}) | |
{path && <CopyButton textToCopy={path} />} | |
</span> | |
<span className="text-gray-500 text-xs transform transition-transform">{isOpen ? '▼' : '►'}</span> | |
</h3> | |
{isOpen && children} | |
</div> | |
); | |
} | |
function PropertyNode({ node, level = 0, parentPath = '' }) { | |
const [isExpanded, setIsExpanded] = useState(false); | |
const [children, setChildren] = useState([]); | |
const [isLoading, setIsLoading] = useState(false); | |
const handleToggle = async () => { | |
if (!node.hasChildren) return; | |
const nextExpandedState = !isExpanded; | |
setIsExpanded(nextExpandedState); | |
if (nextExpandedState && children.length === 0) { | |
setIsLoading(true); | |
try { | |
let result = await rpcClient.getPropertyChildren(node.id); | |
setChildren(result); | |
} catch (e) { | |
alert(`Error fetching property children: ${e.message}`); | |
setIsExpanded(false); | |
} finally { | |
setIsLoading(false); | |
} | |
} | |
}; | |
const isResolvableElement = node.valueType === 'ResolvableElement'; | |
const currentPath = useMemo(() => { | |
// e.g., @['entities'][0] or @['entities'][Name='Account'] | |
if (node.name.startsWith('[')) { | |
return `${parentPath}${node.name}`; | |
} | |
// e.g., @['entities'][Name='Account']['generalization'] | |
return `${parentPath}['${node.name}']`; | |
}, [parentPath, node.name]); | |
return ( | |
<div className="border-b hover:bg-gray-50 text-xs"> | |
<div className="group-hover flex items-center" style={{ paddingLeft: `${level * 16}px` }} onClick={handleToggle}> | |
<div className={`p-1 flex items-center flex-grow ${node.hasChildren ? 'cursor-pointer' : ''}`}> | |
<span className="w-5 h-5 text-center text-gray-500 mr-1 flex-shrink-0"> | |
{node.hasChildren ? (isExpanded ? '▼' : '►') : <span className="inline-block w-3"></span>} | |
</span> | |
<span className="font-mono text-gray-600 break-all w-2/5 pr-2">{node.name}</span> | |
<span className={`break-all w-2/5 pr-2 ${isResolvableElement ? 'italic text-purple-700' : ''}`}> | |
{node.value} | |
</span> | |
<span className="text-gray-500 break-all w-1/5">{node.type}</span> | |
</div> | |
<CopyButton textToCopy={currentPath} /> | |
</div> | |
{isExpanded && ( | |
<div> | |
{isLoading && <div style={{ paddingLeft: `${(level + 1) * 16}px` }} className="text-gray-400 italic p-1">Loading...</div>} | |
{children.map(child => <PropertyNode key={child.id} node={child} level={level + 1} parentPath={currentPath} />)} | |
</div> | |
)} | |
</div> | |
); | |
} | |
function InspectorPanel({ selectedNode, loading }) { | |
if (loading) return <div className="p-4 text-gray-500">Loading details...</div>; | |
if (!selectedNode || !selectedNode.details) return <div className="p-4 text-gray-500">Select a unit to see its details.</div>; | |
const { details } = selectedNode; | |
const { name, type, properties, elements } = details; | |
const groupedElements = elements.reduce((acc, el) => { | |
(acc[el.type] = acc[el.type] || []).push(el); | |
return acc; | |
}, {}); | |
return ( | |
<div className="p-3 bg-white h-full overflow-y-auto"> | |
<h2 className="text-lg font-bold text-gray-800 break-all">{name}</h2> | |
<p className="text-xs text-blue-600 bg-blue-100 rounded px-2 py-0.5 inline-block mb-4">{type}</p> | |
<CollapsibleSection title="Internal Elements" count={elements.length} defaultOpen={false}> | |
{elements.length > 0 ? ( | |
Object.entries(groupedElements).map(([elementType, elementList]) => { | |
// e.g., @DomainModels$Entity | |
const groupPath = `@${elementType}`; | |
// e.g., @DomainModels$Entity[Name='Account']['generalization'] | |
const groupPathWithName = elementList.length === 1 ? `@${elementType}[Name='${elementList[0].name}']` : groupPath; | |
return ( | |
<CollapsibleSection key={elementType} title={elementType} count={elementList.length} path={groupPath}> | |
{elementList.map((el) => { | |
const hasName = el.name && !el.name.startsWith('['); | |
// e.g., @DomainModels$Entity[Name='Account'] | |
const elIdentifier = hasName ? `[Name='${el.name}']` : `[${el.name.replace(/[\[\]]/g, '')}]`; | |
const elPath = `${groupPath}${elIdentifier}`; | |
return <PropertyNode key={el.id} node={el} level={1} parentPath={elPath} />; | |
})} | |
</CollapsibleSection> | |
); | |
}) | |
) : <p className="text-xs text-gray-400 italic">No internal elements found.</p>} | |
</CollapsibleSection> | |
<CollapsibleSection title="Properties" count={properties.length} defaultOpen={true}> | |
{properties.length > 0 ? ( | |
<div className="w-full text-left text-xs"> | |
<div className="border-b border-t flex bg-gray-50 sticky top-0"> | |
<div className="font-semibold p-1 w-2/5 pl-8">Name</div> | |
<div className="font-semibold p-1 w-2/5">Value</div> | |
<div className="font-semibold p-1 w-1/5">Type</div> | |
</div> | |
<div> | |
{properties.map(prop => <PropertyNode key={prop.id} node={prop} parentPath="@" />)} | |
</div> | |
</div> | |
) : <p className="text-xs text-gray-400 italic">No properties found.</p>} | |
</CollapsibleSection> | |
</div> | |
); | |
} | |
function TreeNode({ node, onSelect, selectedNodeId, level = 0, parentPath = '' }) { | |
const [isExpanded, setIsExpanded] = useState(false); | |
const [children, setChildren] = useState([]); | |
const [isLoading, setIsLoading] = useState(false); | |
const currentPath = useMemo(() => { | |
const namePart = node.name ? `[Name='${node.name}']` : `[${node.index ?? 0}]`; | |
if (node.isUnit) { | |
return `${parentPath}/${node.type}${namePart}`; | |
} | |
return `${parentPath}/${node.type}${namePart}`; | |
}, [node, parentPath]); | |
const handleToggle = async () => { | |
const nextExpandedState = !isExpanded; | |
setIsExpanded(nextExpandedState); | |
if (nextExpandedState && children.length === 0 && node.hasChildren) { | |
setIsLoading(true); | |
try { | |
const result = await rpcClient.getNodeChildren(node.id); | |
setChildren(result); | |
} catch (e) { | |
alert(`Error fetching children: ${e.message}`); | |
setIsExpanded(false); | |
} finally { | |
setIsLoading(false); | |
} | |
} | |
}; | |
const isSelected = selectedNodeId === node.id; | |
const nodeTypeColor = node.isUnit ? "text-blue-700" : "text-gray-600"; | |
const handleSelect = () => { | |
onSelect({ id: node.id, path: currentPath }); | |
}; | |
return ( | |
<div style={{ paddingLeft: `${level * 16}px` }}> | |
<div onClick={handleSelect} className={`group-hover flex items-center p-1 rounded cursor-pointer ${isSelected ? 'bg-blue-200' : 'hover:bg-gray-200'}`}> | |
<button onClick={(e) => { e.stopPropagation(); handleToggle(); }} className="w-5 h-5 text-center text-gray-500 mr-1 flex-shrink-0" disabled={!node.hasChildren}> | |
{node.hasChildren ? (isExpanded ? '▼' : '►') : <span className="inline-block w-2"></span>} | |
</button> | |
<div className="flex-grow overflow-hidden flex items-center"> | |
<span className="font-semibold truncate">{node.name}</span> | |
<span className={`ml-2 text-xs truncate ${nodeTypeColor}`}>({node.type})</span> | |
</div> | |
<CopyButton textToCopy={currentPath} /> | |
</div> | |
{isExpanded && ( | |
<div> | |
{isLoading && <div style={{ paddingLeft: `16px` }} className="text-gray-400 italic">Loading...</div>} | |
{children.map(child => <TreeNode key={child.id} node={child} onSelect={onSelect} selectedNodeId={selectedNodeId} level={level + 1} parentPath={currentPath} />)} | |
</div> | |
)} | |
</div> | |
); | |
} | |
function App() { | |
const [rootNodes, setRootNodes] = useState([]); | |
const [selectedNode, setSelectedNode] = useState(null); | |
const [detailsLoading, setDetailsLoading] = useState(false); | |
useEffect(() => { rpcClient.getNodeChildren('root').then(setRootNodes); }, []); | |
const handleSelectNode = useCallback((nodeInfo) => { | |
if (selectedNode && nodeInfo.id === selectedNode.id) return; | |
const newNodeState = { id: nodeInfo.id, path: nodeInfo.path, details: null }; | |
setSelectedNode(newNodeState); | |
setDetailsLoading(true); | |
rpcClient.getNodeDetails(nodeInfo.id) | |
.then(details => { | |
setSelectedNode({ ...newNodeState, details }); | |
}) | |
.catch(e => alert(`Error getting details: ${e.message}`)) | |
.finally(() => setDetailsLoading(false)); | |
}, [selectedNode]); | |
return ( | |
<div className="flex h-screen w-screen text-gray-800"> | |
<div className="w-1/3 h-full border-r bg-white overflow-y-auto p-2"> | |
<h1 className="text-xl font-bold p-2">Mendix Model Browser</h1> | |
{rootNodes.map(node => ( | |
<TreeNode key={node.id} node={node} onSelect={handleSelectNode} selectedNodeId={selectedNode?.id} parentPath="/" /> | |
))} | |
</div> | |
<div className="w-2/3 h-full overflow-hidden"> | |
<InspectorPanel selectedNode={selectedNode} loading={detailsLoading} /> | |
</div> | |
</div> | |
); | |
} | |
const root = ReactDOM.createRoot(document.getElementById('app')); | |
root.render(<App />); | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# gh gist edit 536df8e04cd5574946a62414abb12ad8 .\main.py -f main.py | |
# pip install pythonnet dependency-injector | |
# === 0. BOILERPLATE & IMPORTS === | |
import clr | |
from dependency_injector import containers, providers | |
import json | |
from typing import Any, Dict, List, Protocol | |
import traceback | |
import inspect | |
clr.AddReference("Mendix.StudioPro.ExtensionsAPI") | |
# PostMessage("backend:clear", '') # 在开发时取消注释以清空日志 | |
# ShowDevTools() # 在开发时取消注释以打开开发者工具 | |
# === 1. CORE UTILITIES === | |
def serialize_json_object(json_object: Any) -> str: | |
# 将.NET对象序列化为JSON字符串 | |
import System.Text.Json | |
return System.Text.Json.JsonSerializer.Serialize(json_object) | |
def deserialize_json_string(json_string: str) -> Any: | |
# 将JSON字符串反序列化为Python对象 | |
return json.loads(json_string) | |
def post_message(channel: str, message: str): | |
# 向前端发送消息的辅助函数 | |
PostMessage(channel, message) | |
# === 2. MODEL BROWSER IMPLEMENTATION === | |
TOP_LEVEL_UNIT_TYPES = [ | |
'Projects$ProjectConversion', | |
'Settings$ProjectSettings', | |
'Texts$SystemTextCollection', | |
'Navigation$NavigationDocument', | |
'Security$ProjectSecurity', | |
'Projects$Module', | |
] | |
MODULE_SPECIAL_UNIT_TYPES = [ | |
'DomainModels$DomainModel', | |
'Security$ModuleSecurity', | |
'Projects$ModuleSettings', | |
] | |
class ModelBrowserService: | |
""" | |
封装所有与模型浏览相关的后端逻辑。 | |
""" | |
def __init__(self, root: Any): | |
self._root = root | |
self._element_cache = {} | |
def _serialize_element_summary(self, element: Any, parent_id: str, index: int) -> Dict[str, Any]: | |
is_unit = hasattr(element, "ID") | |
element_id = f"{parent_id}:_elements:{index}" | |
if is_unit: | |
element_id = str(element.ID) | |
self._element_cache[element_id] = element | |
has_children = (element.GetUnits().Count > 0) if is_unit else False | |
return { | |
"id": element_id, | |
"name": getattr(element, "Name", "[Unnamed]"), | |
"type": element.Type, | |
"isUnit": is_unit, | |
"hasChildren": has_children | |
} | |
def _get_element_by_id(self, element_id: str) -> Any: | |
if element_id in self._element_cache: | |
return self._element_cache[element_id] | |
raise Exception(f"Element with ID '{element_id}' not found in cache.") | |
def _serialize_property(self, prop: Any, parent_id: str) -> Dict[str, Any]: | |
"""将属性序列化,并包含懒加载所需的信息。""" | |
prop_id = f"{parent_id}:property:{prop.Name}" | |
prop_value = "N/A" | |
has_children = False | |
value_type = "Primitive" | |
prop_type_str = str(prop.Type) | |
try: | |
val = prop.Value | |
if val is None: | |
prop_value = "null" | |
elif prop.IsList: | |
has_children = val.Count > 0 | |
prop_value = f"[{val.Count} items]" | |
value_type = "List" | |
if has_children: | |
self._element_cache[prop_id] = val | |
elif hasattr(val, "ID"): # 这是一个可展开的Unit或Element | |
has_children = True | |
prop_value = getattr(val, "Name", "[Unnamed]") | |
value_type = "Element" | |
self._element_cache[prop_id] = val | |
# --- 新增逻辑开始 --- | |
elif prop_type_str in ["ElementByName", "ElementByQualifiedName"] and isinstance(val, str): | |
has_children = False # 我们在这里不解析它,所以它没有子节点 | |
prop_value = str(val) | |
value_type = "ResolvableElement" # 使用新的类型来标识 | |
# --- 新增逻辑结束 --- | |
else: | |
prop_value = str(val) | |
except Exception: | |
prop_value = "[Error reading value]" | |
return { | |
"id": prop_id, | |
"name": prop.Name, | |
"type": prop_type_str if prop_type_str!='Element' or prop.IsList or val==None else f"Element({val.Type},{val.ID})", # 使用prop_type_str | |
"value": prop_value, | |
"hasChildren": has_children, | |
"valueType": value_type, | |
} | |
def get_node_children(self, node_id: str) -> List[Dict[str, Any]]: | |
children_units = [] | |
if node_id == "root": | |
for unit_type in TOP_LEVEL_UNIT_TYPES: | |
children_units.extend(self._root.GetUnitsOfType(unit_type)) | |
else: | |
target_unit = self._get_element_by_id(node_id) | |
all_descendants = list(target_unit.GetUnits()) | |
if not all_descendants: | |
return [] | |
all_descendant_ids = {str(u.ID) for u in all_descendants} | |
grandchildren_ids = set() | |
for descendant in all_descendants: | |
for grand in descendant.GetUnits(): | |
grandchildren_ids.add(str(grand.ID)) | |
direct_child_ids = all_descendant_ids - grandchildren_ids | |
direct_children = [u for u in all_descendants if str( | |
u.ID) in direct_child_ids] | |
if target_unit.Type == 'Projects$Module': | |
special_units = [] | |
other_direct_children = [] | |
special_unit_types_set = set(MODULE_SPECIAL_UNIT_TYPES) | |
for child in direct_children: | |
if child.Type in special_unit_types_set: | |
special_units.append(child) | |
else: | |
other_direct_children.append(child) | |
children_units.extend(special_units) | |
children_units.extend(other_direct_children) | |
else: | |
children_units.extend(direct_children) | |
serialized_children = [self._serialize_element_summary( | |
unit, node_id, i) for i, unit in enumerate(children_units)] | |
return sorted(serialized_children, key=lambda x: x['name'] or '') | |
def get_node_details(self, node_id: str) -> Dict[str, Any]: | |
"""获取单元详情,属性和内部元素都将形成可懒加载的树。""" | |
if node_id == "root": | |
return {"name": "Project Root", "type": "IModelRoot", "properties": [], "elements": []} | |
target_unit = self._get_element_by_id(node_id) | |
# --- 修改 elements_list 的生成方式 --- | |
elements_list = [] | |
for i, element in enumerate(target_unit.GetElements()): | |
element_id = f"{node_id}:element:{i}" | |
self._element_cache[element_id] = element | |
# 当元素没有名字时,使用其类型和索引作为备用名 | |
element_name = getattr(element, "Name", None) | |
if not element_name: | |
element_name = f"[{element.Type.split('$')[-1]} #{i}]" | |
elements_list.append({ | |
"id": element_id, | |
"name": element_name, | |
"type": element.Type, | |
"value": "", # 值可以是空的,因为主要信息在其子属性中 | |
"hasChildren": True, # 假定所有内部元素都有属性可以查看 | |
"valueType": "Element", | |
}) | |
# --- 修改结束 --- | |
properties_tree = [self._serialize_property( | |
prop, node_id) for prop in target_unit.GetProperties()] | |
return { | |
"id": node_id, | |
"name": getattr(target_unit, "Name", "[Unnamed]"), | |
"type": target_unit.Type, | |
"elements": sorted(elements_list, key=lambda x: x['name'] or ''), | |
"properties": sorted(properties_tree, key=lambda x: x['name'] or '') | |
} | |
def get_property_children(self, property_id: str) -> List[Dict[str, Any]]: | |
"""懒加载属性的子节点。""" | |
children = [] | |
prop_value_obj = self._element_cache.get(property_id) | |
if prop_value_obj is None: | |
return [] | |
# --- 修改开始 --- | |
# 检查它是否像一个列表( duck-typing: 检查是否有'__iter__' 和 'Count',并且不是字符串) | |
is_list_like = hasattr(prop_value_obj, '__iter__') and hasattr(prop_value_obj, 'Count') and not isinstance(prop_value_obj, str) | |
if is_list_like and not hasattr(prop_value_obj, 'ID'): # 确保它不是一个有.Count属性的Unit/Element | |
for i, item in enumerate(prop_value_obj): | |
child_id = f"{property_id}:item:{i}" | |
# 检查列表项是否为基本类型 | |
if isinstance(item, (str, int, float, bool)): | |
children.append({ | |
"id": child_id, | |
"name": f"[{i}]", | |
"type": type(item).__name__, # 显示 'str', 'int', etc. | |
"value": str(item), # 直接显示字符串的值 | |
"hasChildren": False, | |
"valueType": "Primitive" | |
}) | |
# 注意:基本类型不需要缓存,因为它们不能再被展开 | |
else: # 否则,假定它是一个复杂对象 | |
self._element_cache[child_id] = item | |
children.append({ | |
"id": child_id, | |
"name": f"[{i}]", | |
"type": getattr(item, "Type", "Unknown"), | |
"value": getattr(item, "Name", "[Unnamed]"), | |
"hasChildren": hasattr(item, "ID") or (hasattr(item, "GetProperties") and any(item.GetProperties())), | |
"valueType": "Element" | |
}) | |
elif hasattr(prop_value_obj, 'ID'): # 是一个Element/Unit | |
# 属性的值本身就是一个可展开的对象 | |
for prop in prop_value_obj.GetProperties(): | |
children.append(self._serialize_property(prop, property_id)) | |
# --- 修改结束 --- | |
# 列表项通常按索引排序,所以这里可以不排序,或按'name'(即索引)排序 | |
# return sorted(children, key=lambda x: x['name'] or '') | |
return children | |
# === 3. RPC & IoC Container === | |
class IRpcModule(Protocol): | |
pass | |
class ModelBrowserRpcModule(IRpcModule): | |
def __init__(self, service: ModelBrowserService): self._service = service | |
def getNodeChildren( | |
self, nodeId: str = "root") -> List[Dict[str, Any]]: return self._service.get_node_children(nodeId) | |
def getNodeDetails( | |
self, nodeId: str) -> Dict[str, Any]: return self._service.get_node_details(nodeId) | |
def getPropertyChildren( | |
self, propertyId: str) -> List[Dict[str, Any]]: return self._service.get_property_children(propertyId) | |
class RpcDispatcher: | |
def __init__(self, modules: List[IRpcModule]): | |
self._methods: Dict[str, Any] = {} | |
for module_instance in modules: | |
for name, method in inspect.getmembers(module_instance, predicate=inspect.ismethod): | |
if not name.startswith('_'): | |
self._methods[name] = method | |
def handle_request(self, request: Dict[str, Any]) -> Dict[str, Any]: | |
method_name = request.get('method') | |
request_id = request.get('id') | |
if not method_name or method_name not in self._methods: | |
return {'jsonrpc': '2.0', 'error': {'message': f"Method '{method_name}' not found."}, 'requestId': request_id} | |
try: | |
params = request.get('params', {}) | |
result = self._methods[method_name](**params) | |
return {'jsonrpc': '2.0', 'result': result, 'requestId': request_id} | |
except Exception as e: | |
error_message = f"Error in '{method_name}': {e}\n{traceback.format_exc()}" | |
PostMessage("backend:info", error_message) | |
return {'jsonrpc': '2.0', 'error': {'message': error_message}, 'requestId': request_id} | |
class AppContainer(containers.DeclarativeContainer): | |
config = providers.Configuration() | |
model_browser_service = providers.Singleton( | |
ModelBrowserService, root=config.mendix_root) | |
model_browser_module = providers.Singleton( | |
ModelBrowserRpcModule, service=model_browser_service) | |
rpc_modules = providers.List(model_browser_module) | |
dispatcher = providers.Singleton(RpcDispatcher, modules=rpc_modules) | |
# === 4. COMPOSITION ROOT === | |
container = AppContainer() | |
container.config.mendix_root.from_value(root) | |
dispatcher_instance = container.dispatcher() | |
def onMessage(e: Any): | |
if e.Message == "frontend:message": | |
try: | |
message_data = deserialize_json_string(serialize_json_object(e)) | |
request_object = message_data.get("Data") | |
if request_object: | |
response = dispatcher_instance.handle_request(request_object) | |
post_message("backend:response", json.dumps(response)) | |
except Exception as ex: | |
PostMessage( | |
"backend:info", f"Fatal error in onMessage: {ex}\n{traceback.format_exc()}") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"name": "untype visualizer", | |
"author": null, | |
"email": null, | |
"ui": "index.html", | |
"plugin": "main.py" | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment