mirror of
https://github.com/jeremytammik/RevitSdkSamples.git
synced 2026-08-09 22:51:32 +00:00
520 lines
22 KiB
C#
520 lines
22 KiB
C#
//
|
|
// (C) Copyright 2003-2019 by Autodesk, Inc.
|
|
//
|
|
// Permission to use, copy, modify, and distribute this software in
|
|
// object code form for any purpose and without fee is hereby granted,
|
|
// provided that the above copyright notice appears in all copies and
|
|
// that both that copyright notice and the limited warranty and
|
|
// restricted rights notice below appear in all supporting
|
|
// documentation.
|
|
//
|
|
// AUTODESK PROVIDES THIS PROGRAM "AS IS" AND WITH ALL FAULTS.
|
|
// AUTODESK SPECIFICALLY DISCLAIMS ANY IMPLIED WARRANTY OF
|
|
// MERCHANTABILITY OR FITNESS FOR A PARTICULAR USE. AUTODESK, INC.
|
|
// DOES NOT WARRANT THAT THE OPERATION OF THE PROGRAM WILL BE
|
|
// UNINTERRUPTED OR ERROR FREE.
|
|
//
|
|
// Use, duplication, or disclosure by the U.S. Government is subject to
|
|
// restrictions set forth in FAR 52.227-19 (Commercial Computer
|
|
// Software - Restricted Rights) and DFAR 252.227-7013(c)(1)(ii)
|
|
// (Rights in Technical Data and Computer Software), as applicable.
|
|
//
|
|
|
|
using System;
|
|
using System.Windows.Forms;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.Data;
|
|
|
|
using Autodesk.Revit;
|
|
using Autodesk.Revit.DB;
|
|
using Autodesk.Revit.DB.Events;
|
|
using Autodesk.Revit.DB.Architecture;
|
|
using System.IO;
|
|
|
|
namespace Revit.SDK.Samples.RoomSchedule
|
|
{
|
|
/// <summary>
|
|
/// One struct defines the content of mapped Excel spreadsheet: the full name of this file and the sheet.
|
|
/// Only the opened sheet is reserved by this struct.
|
|
/// </summary>
|
|
public class SheetInfo
|
|
{
|
|
#region Class Member Variables
|
|
/// <summary>
|
|
/// Excel file name, it's full path
|
|
/// </summary>
|
|
string m_fileName;
|
|
|
|
/// <summary>
|
|
/// Sheet table within excel file, it's opened by sample
|
|
/// </summary>
|
|
string m_sheetName;
|
|
#endregion
|
|
|
|
|
|
#region Class Public Methods
|
|
/// <summary>
|
|
/// Ctor method
|
|
/// </summary>
|
|
/// <param name="fileName">Full path name file.</param>
|
|
/// <param name="sheetName">The sheet name of spreadsheet which was opened.</param>
|
|
public SheetInfo(String fileName, String sheetName)
|
|
{
|
|
m_fileName = fileName;
|
|
m_sheetName = sheetName;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get or set the full name of spreadsheet file
|
|
/// </summary>
|
|
public string FileName
|
|
{
|
|
get { return m_fileName; }
|
|
set { m_fileName = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get or set the sheet name within the spreadsheet, the sheet name was mapped by Revit rooms.
|
|
/// </summary>
|
|
public string SheetName
|
|
{
|
|
get { return m_sheetName; }
|
|
set { m_sheetName = value; }
|
|
}
|
|
#endregion
|
|
}
|
|
|
|
/// <summary>
|
|
/// Class consists of delegate methods of DocumentSaving/SavingAs and DocumentClosing events.
|
|
/// These delegates will be raised once document is about to be saved or closed.
|
|
/// But, delegate will update mapped spreadsheet only when user created rooms for current document.
|
|
/// (That's, user clicks the button "Create Unplaced Rooms" and new rooms was created successfully).
|
|
/// Otherwise, these events handler methods won't do any update even if they were raised.
|
|
/// </summary>
|
|
public sealed class EventsReactor : IDisposable
|
|
{
|
|
#region Class Global Static Variables
|
|
/// <summary>
|
|
/// Array of documents' hash code and mapped Excel file and opened table.
|
|
/// The mapped excel and its table will be updated when events DocumentSave/SaveAs are raised.
|
|
/// The update occurs only when new room was created according to excel spreadsheet.
|
|
/// </summary>
|
|
private Dictionary<int, SheetInfo> m_docMapDict = new Dictionary<int, SheetInfo>();
|
|
|
|
/// <summary>
|
|
/// Specified log file name
|
|
/// </summary>
|
|
private String m_logFile;
|
|
|
|
/// <summary>
|
|
/// Logging writer used to write logging to log specified log file.
|
|
/// It's not recommended to access m_logWriter and call it's method, because maybe it's not initialized yet.
|
|
/// Please call DumpLog to dump related logging
|
|
/// </summary>
|
|
private StreamWriter m_logWriter;
|
|
#endregion
|
|
|
|
|
|
#region Class Public Implementations
|
|
/// <summary>
|
|
/// This class will dump information to log file to tell user what happened
|
|
/// </summary>
|
|
/// <param name="logFile"></param>
|
|
public EventsReactor(String logFile)
|
|
{
|
|
m_logFile = logFile;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Release the file handling
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
if (null != m_logWriter)
|
|
{
|
|
// close the stream
|
|
m_logWriter.Flush();
|
|
m_logWriter.Close();
|
|
m_logWriter = null;
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finalizer, we need to ensure the file stream was closed
|
|
/// This destructor will run only if the Dispose method does not get called.
|
|
/// </summary>
|
|
~EventsReactor()
|
|
{
|
|
Dispose();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Delegate for document save as event, it will update spreadsheet if document was mapped to spreadsheet.
|
|
/// </summary>
|
|
/// <param name="sender">Event sender.</param>
|
|
/// <param name="e">EventArgs of this event.</param>
|
|
public void DocumentSavingAs(object sender, DocumentSavingAsEventArgs e)
|
|
{
|
|
DumpLog("Raised DocumentSavingAs -> Document: " + Path.GetFileNameWithoutExtension(e.Document.Title));
|
|
UpdateMappedSpreadsheet(e.Document);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Delegate for document save event, it will update spreadsheet if document was mapped to spreadsheet.
|
|
/// </summary>
|
|
/// <param name="sender">Event sender.</param>
|
|
/// <param name="e">EventArgs of this event.</param>
|
|
public void DocumentSaving(object sender, DocumentSavingEventArgs e)
|
|
{
|
|
DumpLog("Raised DocumentSaving -> Document: " + Path.GetFileNameWithoutExtension(e.Document.Title));
|
|
UpdateMappedSpreadsheet(e.Document);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removed the document which was closed, event reactor doesn't need to monitor this document any more.
|
|
/// DocumentId is designed to identify one document, it's equal to hash code of this document.
|
|
/// </summary>
|
|
/// <param name="sender"></param>
|
|
/// <param name="e"></param>
|
|
public void DocumentClosed(object sender, DocumentClosedEventArgs e)
|
|
{
|
|
DumpLog("Raised DocumentClosed.");
|
|
m_docMapDict.Remove(e.DocumentId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if document is monitored by this event reactor
|
|
/// </summary>
|
|
/// <param name="docHashcode">Hashcode of document.</param>
|
|
/// <returns></returns>
|
|
public bool DocMonitored(int docHashcode)
|
|
{
|
|
return m_docMapDict.ContainsKey(docHashcode);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the sheet information of document.
|
|
/// </summary>
|
|
/// <param name="hashCode">The hash code of document.</param>
|
|
/// <param name="sheetInfo">The mapped spread file and sheet information.</param>
|
|
/// <returns>Indicates whether find the spread sheet mapped by this document.
|
|
/// True if mapped spreadsheet information found, else false.</returns>
|
|
public bool DocMappedSheetInfo(int hashCode, ref SheetInfo sheetInfo)
|
|
{
|
|
if(!DocMonitored(hashCode))
|
|
{
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
return m_docMapDict.TryGetValue(hashCode, out sheetInfo);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update or reset the sheet information to which document is being mapped.
|
|
/// </summary>
|
|
/// <param name="hashCode">Hash code of document used as key to find mapped spreadsheet.</param>
|
|
/// <param name="newSheetInfo">New value for spreadsheet.</param>
|
|
public void UpdateSheeInfo(int hashCode, SheetInfo newSheetInfo)
|
|
{
|
|
if(!DocMonitored(hashCode))
|
|
{
|
|
m_docMapDict.Add(hashCode, newSheetInfo);
|
|
}
|
|
else
|
|
{
|
|
m_docMapDict.Remove(hashCode);
|
|
m_docMapDict.Add(hashCode, newSheetInfo);
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
|
|
#region Class Implementations
|
|
/// <summary>
|
|
/// Update mapped spread sheet when document is about to be saved or saved as
|
|
/// This method will update spread sheet room data([Area] column) with actual area value of mapped Revit Room.
|
|
/// or add Revit room to spreadsheet if it is not mapped to room of spreadsheet. /// </summary>
|
|
/// <param name="activeDocument">Current active document.</param>
|
|
private void UpdateMappedSpreadsheet(Document activeDocument)
|
|
{
|
|
// Programming Routines:
|
|
//
|
|
// 1: Update spreadsheet when:
|
|
// a: there is room work sheet table;
|
|
// b: there is rooms data;
|
|
// c: shared parameter exists;
|
|
// 2: Skip update and insert operations for below rooms:
|
|
// a: the rooms are not placed or located;
|
|
// b: the rooms whose shared parameter(defined by sample) are not retrieved,
|
|
// some rooms maybe don't have shared parameter at all, despite user create for Rooms category.
|
|
// 3: Update spreadsheet rooms values by Revit room actual values.
|
|
// a: if shared parameter exists(is not null), update row by using this parameter's value;
|
|
// b: if shared parameter doesn't exist (is null), update row by Id value of room, which will avoid the duplicate
|
|
// ID columns occur in spreadsheet.
|
|
// 4: Insert Revit rooms data to spreadsheet if:
|
|
// a: failed to update values of rooms (maybe there no matched ID value in spread sheet rows).
|
|
//
|
|
|
|
#region Check Whether Update Spreadsheet Data
|
|
//
|
|
// check which table to be updated.
|
|
SheetInfo mappedXlsAndTable;
|
|
bool hasValue = m_docMapDict.TryGetValue(activeDocument.GetHashCode(), out mappedXlsAndTable);
|
|
if (!hasValue || null == mappedXlsAndTable ||
|
|
String.IsNullOrEmpty(mappedXlsAndTable.FileName) || String.IsNullOrEmpty(mappedXlsAndTable.SheetName))
|
|
{
|
|
DumpLog("This document isn't mapped to spreadsheet yet.");
|
|
return;
|
|
}
|
|
|
|
// retrieve all rooms in project(maybe there are new rooms created manually by user)
|
|
RoomsData roomData = new RoomsData(activeDocument);
|
|
if (roomData.Rooms.Count <= 0)
|
|
{
|
|
DumpLog("This document doesn't have any room yet.");
|
|
return;
|
|
}
|
|
#endregion
|
|
|
|
// create a connection and update values of spread sheet
|
|
int updatedRows = 0; // number of rows which were updated
|
|
int newRows = 0; // number of rows which were added into spread sheet
|
|
XlsDBConnector dbConnector = new XlsDBConnector(mappedXlsAndTable.FileName);
|
|
|
|
// check whether there is room table.
|
|
// get all available rooms in current document once more
|
|
int stepNo = -1;
|
|
DumpLog(System.Environment.NewLine + "Start to update spreadsheet room......");
|
|
foreach (Room room in roomData.Rooms)
|
|
{
|
|
// check Whether We Update This Room
|
|
stepNo++;
|
|
double roomArea = 0.0f;
|
|
String externalId = String.Empty;
|
|
if (!ValidateRevitRoom(activeDocument, room, ref roomArea, ref externalId))
|
|
{
|
|
DumpLog(String.Format("#{0}--> Room:{1} was skipped.", stepNo, room.Number));
|
|
continue;
|
|
}
|
|
|
|
// try to update
|
|
try
|
|
{
|
|
|
|
#region Update Spreadsheet Room
|
|
// flag used to indicate whether update is successful
|
|
bool bUpdateFailed = false; // reserve whether this room updated successfully.
|
|
// if room comment is empty, use <null> for mapped room, use <Added from Revit> for not mapped room in spread sheet.
|
|
bool bCommnetIsNull = false;
|
|
|
|
// get comments of room
|
|
String comments;
|
|
Parameter param = room.get_Parameter(BuiltInParameter.ALL_MODEL_INSTANCE_COMMENTS);
|
|
comments = (null != param) ? (param.AsString()) : ("");
|
|
if (String.IsNullOrEmpty(comments))
|
|
{
|
|
// this room doesn't have comment value
|
|
bCommnetIsNull = true;
|
|
// use <null> for room with empty comment by default when updating spread sheet
|
|
comments = "<null>";
|
|
}
|
|
|
|
// create update SQL clause,
|
|
// when filtering row to be updated, use Room.Id if "External Room ID" is null.
|
|
String updateStr = String.Format(
|
|
"Update [{0}$] SET [{1}] = '{2}', [{3}] = '{4}', [{5}] = '{6}', [{7}] = '{8:N3}' Where [{9}] = {10}",
|
|
mappedXlsAndTable.SheetName, // mapped table name
|
|
RoomsData.RoomName, room.Name,
|
|
RoomsData.RoomNumber, room.Number,
|
|
RoomsData.RoomComments, comments,
|
|
RoomsData.RoomArea, roomArea,
|
|
RoomsData.RoomID, String.IsNullOrEmpty(externalId) ? room.Id.ToString() : externalId);
|
|
|
|
// execute the command and check the size of updated rows
|
|
int afftectedRows = dbConnector.ExecuteCommnand(updateStr);
|
|
if (afftectedRows == 0)
|
|
{
|
|
bUpdateFailed = true;
|
|
}
|
|
else
|
|
{
|
|
// count how many rows were updated
|
|
DumpLog(String.Format("#{0}--> {1}", stepNo, updateStr));
|
|
updatedRows += afftectedRows;
|
|
|
|
// if "External Room ID" is null but update successfully, which means:
|
|
// in spreadsheet there is existing row whose "ID" value equals to room.Id, so we should
|
|
// set Revit room's "External Room ID" value to Room.Id for consistence after update .
|
|
if (String.IsNullOrEmpty(externalId))
|
|
{
|
|
SetExternalRoomIdToRoomId(room);
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
|
|
#region Insert Revit Room
|
|
|
|
// Add this new room to spread sheet if fail to update spreadsheet
|
|
if (bUpdateFailed)
|
|
{
|
|
// try to insert this new room to spread sheet, some rules:
|
|
// a: if the "External Room ID" exists, set ID column to this external id value,
|
|
// if the "External Room ID" doesn't exist, use the actual Revit room id as the ID column value.
|
|
// b: use comments in room if room's description exists,
|
|
// else, use constant string: "<Added from Revit>" for Comments column in spreadsheet.
|
|
|
|
String insertStr =
|
|
String.Format("Insert Into [{0}$] ([{1}], [{2}], [{3}], [{4}], [{5}]) Values('{6}', '{7}', '{8}', '{9}', '{10:N3}')",
|
|
mappedXlsAndTable.SheetName, // mapped table name
|
|
RoomsData.RoomID, RoomsData.RoomComments, RoomsData.RoomName, RoomsData.RoomNumber, RoomsData.RoomArea,
|
|
(String.IsNullOrEmpty(externalId)) ? (room.Id.ToString()) : (externalId), // Room id
|
|
(bCommnetIsNull || String.IsNullOrEmpty(comments)) ? ("<Added from Revit>") : (comments),
|
|
room.Name, room.Number, roomArea);
|
|
|
|
// try to insert it
|
|
afftectedRows = dbConnector.ExecuteCommnand(insertStr);
|
|
if (afftectedRows != 0)
|
|
{
|
|
// remember the number of new rows
|
|
String succeedMsg = String.Format("#{0}--> Succeeded to insert spreadsheet Room - Name:{1}, Number:{2}, Area:{3:N3}",
|
|
stepNo, room.Name, room.Number, roomArea);
|
|
DumpLog(succeedMsg);
|
|
newRows += afftectedRows;
|
|
|
|
// if the Revit room doesn't have external id value(may be a room created manually)
|
|
// set its "External Room ID" value to Room.Id, because the room was added/mapped to spreadsheet,
|
|
// and the value of ID column in sheet is just the Room.Id, we should keep this consistence.
|
|
if (String.IsNullOrEmpty(externalId))
|
|
{
|
|
SetExternalRoomIdToRoomId(room);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
DumpLog(String.Format("#{0}--> Failed: {1}", stepNo, insertStr));
|
|
}
|
|
}
|
|
#endregion
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// close the connection
|
|
DumpLog(String.Format("#{0}--> Exception: {1}", stepNo, ex.Message));
|
|
dbConnector.Dispose();
|
|
RoomScheduleForm.MyMessageBox(ex.Message, MessageBoxIcon.Warning);
|
|
return;
|
|
}
|
|
}
|
|
// close the connection
|
|
dbConnector.Dispose();
|
|
|
|
// output the affected result message
|
|
String sumMsg = String.Format("{0}:[{1}]: {2} rows were updated and {3} rows were added into successfully.",
|
|
Path.GetFileName(mappedXlsAndTable.FileName), mappedXlsAndTable.SheetName, updatedRows, newRows);
|
|
DumpLog(sumMsg);
|
|
DumpLog("Finish updating spreadsheet room." + System.Environment.NewLine);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Check to see if we need to update spreadsheet data according to this Revit room.
|
|
/// We don't need to update spreadsheet rooms if Revit room:
|
|
/// . Which is one unplaced room.
|
|
/// . The room has area which is zero.
|
|
/// . Special room which doesn't have custom shared parameter at all.
|
|
/// </summary>
|
|
/// <param name="activeDocument">Current active document.</param>
|
|
/// <param name="roomObj">Room object to be checked.</param>
|
|
/// <param name="roomArea">Room area of this Revit room.</param>
|
|
/// <param name="externalId">The value of custom shared parameter of this room.</param>
|
|
/// <returns>Indicates whether it succeeded to get room area and shared parameter value.</returns>
|
|
private static bool ValidateRevitRoom(Document activeDocument, Room room, ref double roomArea, ref String externalId)
|
|
{
|
|
roomArea = 0.0f;
|
|
externalId = String.Empty;
|
|
if (null == room.Location || null == activeDocument.GetElement(room.LevelId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// get Area of room, if converting to double value fails, skip this.
|
|
// if the area is zero to less than zero, skip the update too
|
|
try
|
|
{
|
|
// get area without unit, then converting it to double will be ok.
|
|
String areaStr = RoomsData.GetProperty(activeDocument, room, BuiltInParameter.ROOM_AREA, false);
|
|
roomArea = Double.Parse(areaStr);
|
|
if (roomArea <= double.Epsilon)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// parse double value failed, continue the loop
|
|
return false;
|
|
}
|
|
|
|
// get the shared parameter value of room
|
|
Parameter externalIdSharedParam = null;
|
|
bool bExist = RoomsData.ShareParameterExists(room, RoomsData.SharedParam, ref externalIdSharedParam);
|
|
if (false == bExist || null == externalIdSharedParam)
|
|
{
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
externalId = externalIdSharedParam.AsString();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set shared parameter (whose name is "External Room ID") value to Room.Id
|
|
/// </summary>
|
|
/// <param name="room">The room used to get the room which to be updated</param>
|
|
private static bool SetExternalRoomIdToRoomId(Room room)
|
|
{
|
|
try
|
|
{
|
|
Parameter shareParam = room.LookupParameter(RoomsData.SharedParam);
|
|
if (null != shareParam)
|
|
{
|
|
return shareParam.Set(room.Id.ToString());
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// none
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dump log file now
|
|
/// </summary>
|
|
private void DumpLog(String strLog)
|
|
{
|
|
// Create writer only when there is dump
|
|
if(null == m_logWriter) {
|
|
if (File.Exists(m_logFile))
|
|
{
|
|
File.Delete(m_logFile);
|
|
}
|
|
m_logWriter = new StreamWriter(m_logFile);
|
|
m_logWriter.AutoFlush = true;
|
|
}
|
|
//
|
|
// dump log now
|
|
m_logWriter.WriteLine(strLog);
|
|
}
|
|
#endregion
|
|
}
|
|
}
|