Xây dựng Helper Methods trong Revit API

/Bài viết

    Trong bài viết trước, chúng ta đã thảo luận về những thách thức khi làm việc với Parameters trong Revit API, từ việc phân biệt InstanceType Parameters đến xử lý các loại giá trị và đơn vị khác nhau. Để giải quyết sự phức tạp này và làm cho code của bạn trở nên gọn gàng, dễ đọc và dễ bảo trì hơn, việc xây dựng các hàm hỗ trợ (Helper Methods) là một chiến lược không thể thiếu. Bài viết này sẽ hướng dẫn bạn cách tạo ra các hàm tiện ích riêng để đơn giản hóa việc đọc và ghi Parameters một cách hiệu quả.

    Tại sao cần Helper Methods cho Parameters?

    Bạn có thể tự hỏi, tại sao phải mất công viết các hàm riêng khi Revit API đã cung cấp sẵn các phương thức LookupParameter(), AsDouble(), Set()? Dưới đây là những lý do chính:

    • Giảm trùng lặp code (DRY - Don't Repeat Yourself): Thay vì viết lại cùng một đoạn code kiểm tra null, StorageType, IsReadWrite, hoặc xử lý đơn vị mỗi khi bạn cần đọc/ghi một Parameter, bạn chỉ cần gọi một hàm duy nhất.
    • Tăng tính dễ đọc: Code chính của bạn sẽ trông sạch sẽ và tập trung hơn vào logic nghiệp vụ, thay vì bị ngập trong các kiểm tra và chuyển đổi Parameter.
    • Dễ bảo trì và cập nhật: Nếu có thay đổi trong cách Revit API xử lý Parameters (ví dụ: một phiên bản mới thay đổi cách đọc đơn vị), bạn chỉ cần sửa đổi trong Helper Method đó, thay vì phải tìm và sửa ở hàng chục chỗ trong toàn bộ Add-in.
    • Xử lý lỗi tập trung: Bạn có thể tích hợp cơ chế xử lý lỗi (logging, thông báo) vào Helper Method, giúp quản lý lỗi tốt hơn.
    • Code mạnh mẽ hơn: Helper Methods có thể bao gồm các kiểm tra bổ sung (như null checks, StorageType checks) để tránh các lỗi runtime phổ biến.

    Thiết kế các Helper Methods cơ bản

    Chúng ta sẽ xây dựng một lớp tĩnh (static class) tên là ParameterUtils (hoặc tên tương tự) để chứa các hàm hỗ trợ này. Điều này giúp các hàm dễ dàng được gọi từ bất kỳ đâu trong Add-in của bạn.

    Hàm đọc giá trị Parameter (Get Parameter Value)

    Chúng ta cần một hàm có thể đọc giá trị của một Parameter và trả về đúng kiểu dữ liệu, đồng thời xử lý các trường hợp không tìm thấy Parameter hoặc StorageType không khớp.

    using Autodesk.Revit.DB;
    using System;
    using System.Collections.Generic;
    
    public static class ParameterUtils
    {
        /// <summary>
        /// Đọc giá trị của một Parameter dựa trên BuiltInParameter.
        /// Xử lý các loại StorageType khác nhau.
        /// </summary>
        /// <typeparam name="T">Kiểu dữ liệu mong muốn (string, double, int, ElementId).</typeparam>
        /// <param name="element">Element chứa Parameter.</param>
        /// <param name="builtInParam">BuiltInParameter cần đọc.</param>
        /// <returns>Giá trị Parameter dưới dạng kiểu T, hoặc giá trị mặc định của T nếu không tìm thấy/lỗi.</returns>
        public static T GetParameterValue<T>(Element element, BuiltInParameter builtInParam)
        {
            if (element == null) return default(T);
    
            Parameter param = element.get_Parameter(builtInParam);
            if (param == null) return default(T);
    
            try
            {
                if (typeof(T) == typeof(string))
                {
                    return (T)(object)param.AsString();
                }
                else if (typeof(T) == typeof(double))
                {
                    // Luôn trả về giá trị nội bộ của Revit cho double
                    return (T)(object)param.AsDouble();
                }
                else if (typeof(T) == typeof(int))
                {
                    return (T)(object)param.AsInteger();
                }
                else if (typeof(T) == typeof(ElementId))
                {
                    return (T)(object)param.AsElementId();
                }
                // Thêm các loại khác nếu cần (ví dụ: bool cho Yes/No parameters)
                else if (typeof(T) == typeof(bool))
                {
                    return (T)(object)(param.AsInteger() == 1); // Yes/No parameter
                }
            }
            catch (InvalidCastException)
            {
                // Log lỗi hoặc thông báo rằng StorageType không khớp với kiểu T
                System.Diagnostics.Debug.WriteLine($"Warning: StorageType of '{param.Definition?.Name}' does not match expected type {typeof(T).Name}.");
            }
            catch (Exception ex)
            {
                // Xử lý các lỗi khác
                System.Diagnostics.Debug.WriteLine($"Error reading parameter '{param.Definition?.Name}': {ex.Message}");
            }
    
            return default(T);
        }
    
        /// <summary>
        /// Đọc giá trị của một Parameter dựa trên tên (string name).
        /// Hữu ích cho Project/Family Parameters hoặc Shared Parameters khi biết tên.
        /// </summary>
        /// <typeparam name="T">Kiểu dữ liệu mong muốn.</typeparam>
        /// <param name="element">Element chứa Parameter.</param>
        /// <param name="paramName">Tên của Parameter.</param>
        /// <returns>Giá trị Parameter dưới dạng kiểu T, hoặc giá trị mặc định của T.</returns>
        public static T GetParameterValue<T>(Element element, string paramName)
        {
            if (element == null) return default(T);
    
            // Lưu ý: LookupParameter ưu tiên Instance Param trước, sau đó là Type Param.
            Parameter param = element.LookupParameter(paramName);
            if (param == null)
            {
                // Thử tìm Type Parameter nếu chưa có
                ElementType elementType = element.Document.GetElement(element.GetTypeId()) as ElementType;
                if (elementType != null)
                {
                    param = elementType.LookupParameter(paramName);
                }
            }
    
            if (param == null) return default(T);
    
            // Sử dụng logic tương tự như hàm GetParameterValue<T>(Element, BuiltInParameter)
            try
            {
                if (typeof(T) == typeof(string)) return (T)(object)param.AsString();
                if (typeof(T) == typeof(double)) return (T)(object)param.AsDouble();
                if (typeof(T) == typeof(int)) return (T)(object)param.AsInteger();
                if (typeof(T) == typeof(ElementId)) return (T)(object)param.AsElementId();
                if (typeof(T) == typeof(bool)) return (T)(object)(param.AsInteger() == 1);
            }
            catch (InvalidCastException)
            {
                System.Diagnostics.Debug.WriteLine($"Warning: StorageType of '{param.Definition?.Name}' (by string name) does not match expected type {typeof(T).Name}.");
            }
            catch (Exception ex)
            {
                System.Diagnostics.Debug.WriteLine($"Error reading parameter '{param.Definition?.Name}' (by string name): {ex.Message}");
            }
            return default(T);
        }
    }
    

    Hàm ghi giá trị Parameter (Set Parameter Value)

    Hàm ghi giá trị cần kiểm tra IsReadWrite và đảm bảo việc chuyển đổi đơn vị nếu cần.

    // Tiếp tục trong class ParameterUtils
    public static class ParameterUtils
    {
        // ... các hàm GetParameterValue ở trên ...
    
        /// <summary>
        /// Ghi giá trị vào một Parameter.
        /// Yêu cầu phải nằm trong một Transaction.
        /// </summary>
        /// <param name="element">Element chứa Parameter.</param>
        /// <param name="builtInParam">BuiltInParameter cần ghi.</param>
        /// <param name="value">Giá trị mới. Đối với double, giả định là giá trị nội bộ (internal units) hoặc đã được chuyển đổi.</param>
        /// <returns>True nếu thành công, False nếu lỗi hoặc không thể ghi.</returns>
        public static bool SetParameterValue(Element element, BuiltInParameter builtInParam, object value)
        {
            if (element == null) return false;
    
            Parameter param = element.get_Parameter(builtInParam);
            if (param == null || !param.IsReadWrite)
            {
                System.Diagnostics.Debug.WriteLine($"Warning: Parameter '{builtInParam}' not found or is read-only.");
                return false;
            }
    
            try
            {
                if (value is string stringVal)
                {
                    return param.Set(stringVal);
                }
                else if (value is double doubleVal)
                {
                    return param.Set(doubleVal); // Giả định doubleVal là giá trị nội bộ
                }
                else if (value is int intVal)
                {
                    return param.Set(intVal);
                }
                else if (value is ElementId elementIdVal)
                {
                    return param.Set(elementIdVal);
                }
                else if (value is bool boolVal)
                {
                    return param.Set(boolVal ? 1 : 0); // Yes/No parameter
                }
                else
                {
                    System.Diagnostics.Debug.WriteLine($"Error: Unsupported value type for parameter '{builtInParam}'.");
                    return false;
                }
            }
            catch (Exception ex)
            {
                System.Diagnostics.Debug.WriteLine($"Error setting parameter '{builtInParam}': {ex.Message}");
                return false;
            }
        }
    
        /// <summary>
        /// Ghi giá trị vào một Parameter dựa trên tên (string name).
        /// Yêu cầu phải nằm trong một Transaction.
        /// </summary>
        /// <param name="element">Element chứa Parameter.</param>
        /// <param name="paramName">Tên của Parameter.</param>
        /// <param name="value">Giá trị mới.</param>
        /// <returns>True nếu thành công, False nếu lỗi hoặc không thể ghi.</returns>
        public static bool SetParameterValue(Element element, string paramName, object value)
        {
            if (element == null) return false;
    
            Parameter param = element.LookupParameter(paramName);
            if (param == null)
            {
                // Thử tìm Type Parameter nếu chưa có
                ElementType elementType = element.Document.GetElement(element.GetTypeId()) as ElementType;
                if (elementType != null)
                {
                    param = elementType.LookupParameter(paramName);
                }
            }
    
            if (param == null || !param.IsReadWrite)
            {
                System.Diagnostics.Debug.WriteLine($"Warning: Parameter '{paramName}' not found or is read-only.");
                return false;
            }
    
            // Sử dụng logic tương tự như hàm SetParameterValue(Element, BuiltInParameter, object)
            try
            {
                if (value is string stringVal) return param.Set(stringVal);
                if (value is double doubleVal) return param.Set(doubleVal);
                if (value is int intVal) return param.Set(intVal);
                if (value is ElementId elementIdVal) return param.Set(elementIdVal);
                if (value is bool boolVal) return param.Set(boolVal ? 1 : 0);
            }
            catch (Exception ex)
            {
                System.Diagnostics.Debug.WriteLine($"Error setting parameter '{paramName}': {ex.Message}");
            }
            return false;
        }
    }
    

    Cách sử dụng Helper Methods hiệu quả

    Với các Helper Methods đã tạo, code của bạn sẽ trở nên sạch sẽ hơn rất nhiều:

    using Autodesk.Revit.DB;
    using Autodesk.Revit.UI;
    using System;
    using System.Linq;
    
    [Autodesk.Revit.Attributes.Transaction(Autodesk.Revit.Attributes.TransactionMode.Manual)]
    public class MyParameterCommand : IExternalCommand
    {
        public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
        {
            UIDocument uidoc = commandData.Application.ActiveUIDocument;
            Document doc = uidoc.Document;
    
            using (Transaction tr = new Transaction(doc, "Thao tác với Parameter qua Helper"))
            {
                tr.Start();
    
                try
                {
                    // Lấy một đối tượng tường ngẫu nhiên
                    Wall wall = new FilteredElementCollector(doc)
                        .OfClass(typeof(Wall))
                        .Cast<Wall>()
                        .FirstOrDefault();
    
                    if (wall != null)
                    {
                        // ĐỌC PARAMETER
                        // Đọc chiều cao tường
                        double currentHeight = ParameterUtils.GetParameterValue<double>(wall, BuiltInParameter.WALL_HEIGHT_PARAM);
                        TaskDialog.Show("Đọc Parameter", $"Chiều cao hiện tại của tường '{wall.Name}': {currentHeight} (Internal Units)");
    
                        // Đọc Comment của tường
                        string wallComment = ParameterUtils.GetParameterValue<string>(wall, BuiltInParameter.ALL_MODEL_INSTANCE_COMMENTS);
                        TaskDialog.Show("Đọc Parameter", $"Comment của tường: '{wallComment}'");
    
                        // GHI PARAMETER
                        // Đặt chiều cao mới là 4 mét (chuyển đổi sang Internal Units trước khi truyền vào Helper)
                        double newHeightInMeters = 4.0;
                        double newHeightInInternalUnits = UnitUtils.Convert(newHeightInMeters, UnitTypeId.Meters, UnitTypeId.Feet);
                        bool successHeight = ParameterUtils.SetParameterValue(wall, BuiltInParameter.WALL_HEIGHT_PARAM, newHeightInInternalUnits);
    
                        // Đặt Comment mới
                        bool successComment = ParameterUtils.SetParameterValue(wall, BuiltInParameter.ALL_MODEL_INSTANCE_COMMENTS, "Đã chỉnh sửa bằng Add-in.");
    
                        if (successHeight && successComment)
                        {
                            TaskDialog.Show("Ghi Parameter", "Đã cập nhật chiều cao và comment của tường thành công!");
                        }
                        else
                        {
                            TaskDialog.Show("Ghi Parameter", "Có lỗi xảy ra khi cập nhật Parameter.");
                        }
                    }
                    else
                    {
                        TaskDialog.Show("Thông báo", "Không tìm thấy tường nào trong mô hình.");
                    }
    
                    tr.Commit();
                }
                catch (Exception ex)
                {
                    tr.RollBack();
                    message = "Lỗi khi chạy lệnh: " + ex.Message;
                    return Result.Failed;
                }
            }
            return Result.Succeeded;
        }
    }
    

    Nâng cao các Helper Methods

    Các Helper Methods trên là điểm khởi đầu tuyệt vời. Bạn có thể mở rộng chúng để xử lý các tình huống phức tạp hơn:

    • Xử lý đơn vị tự động: Thêm các hàm GetParameterValueAsDisplayUnits<T>()SetParameterValueFromDisplayUnits<T>() để Helper Methods tự động xử lý chuyển đổi giữa đơn vị nội bộ và đơn vị hiển thị, dựa trên ForgeTypeId cho đơn vị.
    • Hỗ trợ Shared Parameters theo GUID: Tạo các hàm chuyên biệt để truy cập Shared Parameters bằng GUID, vì đây là cách đáng tin cậy nhất.
    • Xử lý Yes/No Parameters: Cụ thể hóa việc đọc/ghi các tham số boolean (Yes/No) vì chúng được lưu trữ dưới dạng số nguyên (0 hoặc 1).
    • Kiểm tra loại tham số: Thêm logic để kiểm tra xem Parameter có phải là Instance hay Type và truy cập đúng cách.
    • Logging chi tiết: Thay vì chỉ Debug.WriteLine, sử dụng một hệ thống logging thích hợp để ghi lại các cảnh báo hoặc lỗi một cách có tổ chức.
    • Tạo Extension Methods: Để code trông đẹp hơn, bạn có thể biến các Helper Methods này thành Extension Methods cho lớp Element. Ví dụ: wall.GetParameterValue<double>(BuiltInParameter.WALL_HEIGHT_PARAM).

    Bạn đã có những Helper Methods nào thú vị trong dự án Revit API của mình? Hãy chia sẻ chúng ở phần bình luận nhé.