Within Revit, there is the possibility of locating toilets or handwash, using the option within the system and plumbing fixture. My question is is there the possibility of creating them using the Revit api? The only thing I've seen is the creation of some types of systems as shown by the following line of code:
public bool createHotWater(Connector baseConector, ConnectorSet set,
Document doc)
{
try
{
using (var trans = new Transaction(doc, "SystemHotWater"))
{
trans.Start();
PipingSystem piping = doc.Create.
NewPipingSystem(baseConector,
set,PipeSystemType.DomesticHotWater);
trans.Commit();
}
return true;
}
catch (Exception e)
{
return false;
}
}
I know that the above code only creates a hot water system, but I would like to know if there is an option to create toilets from the Revit api.
Yes, certainly this can be achieved programmatically as well as manually through the user interface. For this very purpose, the Family API for Creating Family Definitions was introduced in Revit 2010.
Related
I have a modification that facilitates an internal business process utilizing PO and SO. The screen provides the ability to purchase an MRO spare part outside of the normal replenishment process. The item may or may not be maintained in inventory, so it may be ordered for replenishment or as an SO to be processed as Mark for PO.
In creating the SO, I am able to store the SO reference information to the DAC for my customization. When creating the PO directly, I also am able to capture the PO reference information. However, when creating the PO from the SO using the standard Acumatica menu action for Create Purchase Order, I have been unable to capture the right event to enable storing the PO reference being assigned in SOLineSplit3 to my custom DAC. (Worth noting that I also need to be able to override the default curyunitcost value on the PO line using the value stored on my custom DAC as these purchases do not carry a fixed price per buy. This is done by tracing the SOLineSplit back to my custom DAC and overriding POLine_CuryUnitCost_FieldDefaulting.)
The action invoked on the Sales Order Entry screen (Action - Create Purchase Order) calls the method CreatePOOrders in POCreate.cs which in turn creates an instance of the POOrderEntry graph to create the actual purchase order.
Eventually, the following code is reached, which appears to attach the PO reference information to SOLineSplit3 as soline:
soline.POType = line.OrderType;
soline.PONbr = line.OrderNbr;
soline.POLineNbr = line.LineNbr;
soline.RefNoteID = docgraph.Document.Current.NoteID;
docgraph.UpdateSOLine(soline, docgraph.Document.Current.VendorID, true);
docgraph.FixedDemand.Cache.SetStatus(soline, PXEntryStatus.Updated);
I am not yet familiar with Cache.SetStatus, but the pop-up description seems to indicate that this is using the FixedDemand select in POOrderEntry to find and set (or insert) the SOLineSplit3 record. The call to UpdateSOLine above it is a new internal method that was not in my previous version of POCrete.cs, as this entire method seems to have had some significant rework recently. In trying to capture events on SOLineSplit3 in both POCreate and POOrderEntry, it appears that Cache.SetStatus does not raise any events that I can capture... or I am just completely lost on what event to capture/override.
Immediately following this section, the following appears to update a Replenishment record and save the entire POOrderEntry graph.
if (docgraph.Transactions.Cache.IsInsertedUpdatedDeleted)
{
using (PXTransactionScope scope = new PXTransactionScope())
{
docgraph.Save.Press();
if (demand.PlanType == INPlanConstants.Plan90)
{
docgraph.Replenihment.Current = docgraph.Replenihment.Search<INReplenishmentOrder.noteID>(demand.RefNoteID);
if (docgraph.Replenihment.Current != null)
{
INReplenishmentLine rLine =
PXCache<INReplenishmentLine>.CreateCopy(docgraph.ReplenishmentLines.Insert(new INReplenishmentLine()));
rLine.InventoryID = line.InventoryID;
...
rLine.PlanID = demand.PlanID;
docgraph.ReplenishmentLines.Update(rLine);
docgraph.Caches[typeof(INItemPlan)].Delete(demand);
docgraph.Save.Press();
}
}
scope.Complete();
}
...
}
Basically, I need to insert my code right between the assignment of the PO information to "soline" and the docgraph.Save.Press(); without copying dozens of lines of code to modify this method. I have managed cloning the base method and inserting my code successfully, but I'd prefer to use an event handler and eliminate modifying the standard code. But the question... What event in which graph will let me grab the PO information and follow the breadcumbs back through SOLineSplit to my custom DAC?
Acumatica Build 18.212.0033
Extend POCreate graph because it is the one instanciating the POOrderEntry you are interested in.
Setup a hook on any POOrderEntry graph created by POCreate and subscribe your events on the intercepted graph. I tested this solution, with a Sales Order that has allocations lines in allocation window it will catch the SOLineSplit3 events:
public class POCreate_Extension : PXGraphExtension<POCreate>
{
public override void Initialize()
{
PXGraph.InstanceCreated.AddHandler<POOrderEntry>((graph) =>
{
graph.RowInserting.AddHandler<POOrder>((sender, e) =>
{
PXTrace.WriteInformation("POOrderEntry_POOrder_RowInserting");
});
graph.RowInserting.AddHandler<POOrderEntry.SOLineSplit3>((sender, e) =>
{
PXTrace.WriteInformation("POOrderEntry_SOLineSplit3_RowInserting");
});
graph.RowUpdating.AddHandler<POOrderEntry.SOLineSplit3>((sender, e) =>
{
PXTrace.WriteInformation("POOrderEntry_SOLineSplit3_RowUpdating");
});
graph.RowPersisting.AddHandler<POOrderEntry.SOLineSplit3>((sender, e) =>
{
PXTrace.WriteInformation("POOrderEntry_SOLineSplit3_RowPersisting");
});
});
}
}
I want to create a custom Kofax module. When it comes to the batch processing the scanned documents get converted to PDF files. I want to fetch these PDF files, manipulate them (add a custom footer to the PDF document) and hand them back to Kofax.
So what I know so far:
create Kofax export scripts
add a custom module to Kofax
I have the APIRef.chm (Kofax.Capture.SDK.CustomModule) and the CMSplit as an example project. Unfortunately I struggle getting into it. Are there any resources out there showing step by step how to get into custom module development?
So I know that the IBatch interface represents one selected batch and the IBatchCollection represents the collection of all batches.
I would just like to know how to setup a "Hello World" example and could add my code to it and I think I don't even need a WinForms application because I only need to manipulate the PDF files and that's it...
Since I realized that your question was rather about how to create a custom module in general, allow me to add another answer. Start with a C# Console Application.
Add Required Assemblies
Below assemblies are required by a custom module. All of them reside in the KC's binaries folder (by default C:\Program Files (x86)\Kofax\CaptureSS\ServLib\Bin on a server).
Setup Part
Add a new User Control and Windows Form for setup. This is purely optional - a CM might not even have a setup form, but I'd recommend adding it regardless. The user control is the most important part, here - it will add the menu entry in KC Administration, and initialize the form itself:
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface ISetupForm
{
[DispId(1)]
AdminApplication Application { set; }
[DispId(2)]
void ActionEvent(int EventNumber, object Argument, out int Cancel);
}
[ClassInterface(ClassInterfaceType.None)]
[ProgId("Quipu.KC.CM.Setup")]
public class SetupUserControl : UserControl, ISetupForm
{
private AdminApplication adminApplication;
public AdminApplication Application
{
set
{
value.AddMenu("Quipu.KC.CM.Setup", "Quipu.KC.CM - Setup", "BatchClass");
adminApplication = value;
}
}
public void ActionEvent(int EventNumber, object Argument, out int Cancel)
{
Cancel = 0;
if ((KfxOcxEvent)EventNumber == KfxOcxEvent.KfxOcxEventMenuClicked && (string)Argument == "Quipu.KC.CM.Setup")
{
SetupForm form = new SetupForm();
form.ShowDialog(adminApplication.ActiveBatchClass);
}
}
}
Runtime Part
Since I started with a console application, I could go ahead and put all the logic into Program.cs. Note that is for demo-purposes only, and I would recommend adding specific classes and forms later on. The example below logs into Kofax Capture, grabs the next available batch, and just outputs its name.
class Program
{
static void Main(string[] args)
{
AppDomain.CurrentDomain.AssemblyResolve += (sender, eventArgs) => KcAssemblyResolver.Resolve(eventArgs);
Run(args);
return;
}
static void Run(string[] args)
{
// start processing here
// todo encapsulate this to a separate class!
// login to KC
var login = new Login();
login.EnableSecurityBoost = true;
login.Login();
login.ApplicationName = "Quipu.KC.CM";
login.Version = "1.0";
login.ValidateUser("Quipu.KC.CM.exe", false, "", "");
var session = login.RuntimeSession;
// todo add timer-based polling here (note: mutex!)
var activeBatch = session.NextBatchGet(login.ProcessID);
Console.WriteLine(activeBatch.Name);
activeBatch.BatchClose(
KfxDbState.KfxDbBatchReady,
KfxDbQueue.KfxDbQueueNext,
0,
"");
session.Dispose();
login.Logout();
}
}
Registering, COM-Visibility, and more
Registering a Custom Module is done via RegAsm.exe and ideally with the help of an AEX file. Here's an example - please refer to the documentation for more details and all available settings.
[Modules]
Minimal CM
[Minimal CM]
RuntimeProgram=Quipu/CM/Quipu.KC.CM/Quipu.KC.CM.exe
ModuleID=Quipu.KC.CM.exe
Description=Minimal Template for a Custom Module in C#
Version=1.0
SupportsTableFields=True
SupportsNonImageFiles=True
SetupProgram=Minimal CM Setup
[Setup Programs]
Minimal CM Setup
[Minimal CM Setup]
Visible=0
OCXFile=Quipu/CM/Quipu.KC.CM/Quipu.KC.CM.exe
ProgID=Quipu.KC.CM.Setup
Last but not least, make sure your assemblies are COM-visible:
I put up the entire code on GitHub, feel free to fork it. Hope it helps.
Kofax exposes a batch as an XML, and DBLite is basically a wrapper for said XML. The structure is explained in AcBatch.htm and AcDocs.htm (to be found under the CaptureSV directory). Here's the basic idea (just documents are shown):
AscentCaptureRuntime
Batch
Documents
Document
A single document has child elements itself such as pages, and multiple properties such as Confidence, FormTypeName, and PDFGenerationFileName. This is what you want. Here's how you would navigate down the document collection, storing the filename in a variable named pdfFileName:
IACDataElement runtime = activeBatch.ExtractRuntimeACDataElement(0);
IACDataElement batch = runtime.FindChildElementByName("Batch");
var documents = batch.FindChildElementByName("Documents").FindChildElementsByName("Document");
for (int i = 0; i < documents.Count; i++)
{
// 1-based index in kofax
var pdfFileName = documents[i + 1]["PDFGenerationFileName"];
}
Personally, I don't like this structure, so I created my own wrapper for their wrapper, but that's up to you.
With regard to the custom module itself, the sample shipped is already a decent start. Basically, you would have a basic form that shows up if the user launches the module manually - which is entirely optional if work happens in the back, preferably as Windows Service. I like to start with a console application, adding forms only when needed. Here, I would launch the form as follows, or start the service. Note that I have different branches in case the user wants to install my Custom Module as service:
else if (Environment.UserInteractive)
{
// run as module
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new RuntimeForm(args));
}
else
{
// run as service
ServiceBase.Run(new CustomModuleService());
}
}
The runtime for itself just logs you into Kofax Capture, registers event handlers, and processes batch by batch:
// login to KC
cm = new CustomModule();
cm.Login("", "");
// add progress event handlers
cm.BatchOpened += Cm_BatchOpened;
cm.BatchClosed += Cm_BatchClosed;
cm.DocumentOpened += Cm_DocumentOpened;
cm.DocumentClosed += Cm_DocumentClosed;
cm.ErrorOccured += Cm_ErrorOccured;
// process in background thread so that the form does not freeze
worker = new BackgroundWorker();
worker.DoWork += (s, a) => Process();
worker.RunWorkerAsync();
Then, your CM fetches the next batch. This can either make use of Kofax' Batch Notification Service, or be based on a timer. For the former, just handle the BatchAvailable event of the session object:
session.BatchAvailable += Session_BatchAvailable;
For the latter, define a timer - preferrably with a configurable polling interval:
pollTimer.Interval = pollIntervalSeconds * 1000;
pollTimer.Elapsed += PollTimer_Elapsed;
pollTimer.Enabled = true;
When the timer elapses, you could do the following:
private void PollTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
mutex.WaitOne();
ProcessBatches();
mutex.ReleaseMutex();
}
I would like to use the Autodesk Design Automation API to extract all Text and Header information from a .dwg file into a json object. Is this possible with the Design Automation API?
Any example would help.
Thankyou
#Kaliph, yes, without a plugin in .NET/C++/Lisp code, it is impossible to extract block attributes by script only. I'd recommend .NET. It would be easier for you to get started with if you are not familiar with C++.
Firstly, I'd suggest you take a look at the training labs of AutoCAD .NET API:
https://www.autodesk.com/developer-network/platform-technologies/autocad
pick the latest version if you installed a latest version of AutoCAD. The main workflow of API is same across different versions, though. you can also pick C++ (ObjectARX) if you like.
In the tutorials above, it demos how to work with block. And the blog below talks about how to get attributes:
http://through-the-interface.typepad.com/through_the_interface/2006/09/getting_autocad.html
I copied here for convenience:
using Autodesk.AutoCAD;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
namespace MyApplication
{
public class DumpAttributes
{
[CommandMethod("LISTATT")]
public void ListAttributes()
{
Editor ed =
Application.DocumentManager.MdiActiveDocument.Editor;
Database db =
HostApplicationServices.WorkingDatabase;
Transaction tr =
db.TransactionManager.StartTransaction();
// Start the transaction
try
{
// Build a filter list so that only
// block references are selected
TypedValue[] filList = new TypedValue[1] {
new TypedValue((int)DxfCode.Start, "INSERT")
};
SelectionFilter filter =
new SelectionFilter(filList);
PromptSelectionOptions opts =
new PromptSelectionOptions();
opts.MessageForAdding = "Select block references: ";
PromptSelectionResult res =
ed.GetSelection(opts, filter);
// Do nothing if selection is unsuccessful
if (res.Status != PromptStatus.OK)
return;
SelectionSet selSet = res.Value;
ObjectId[] idArray = selSet.GetObjectIds();
foreach (ObjectId blkId in idArray)
{
BlockReference blkRef =
(BlockReference)tr.GetObject(blkId,
OpenMode.ForRead);
BlockTableRecord btr =
(BlockTableRecord)tr.GetObject(
blkRef.BlockTableRecord,
OpenMode.ForRead
);
ed.WriteMessage(
"\nBlock: " + btr.Name
);
btr.Dispose();
AttributeCollection attCol =
blkRef.AttributeCollection;
foreach (ObjectId attId in attCol)
{
AttributeReference attRef =
(AttributeReference)tr.GetObject(attId,
OpenMode.ForRead);
string str =
("\n Attribute Tag: "
+ attRef.Tag
+ "\n Attribute String: "
+ attRef.TextString
);
ed.WriteMessage(str);
}
}
tr.Commit();
}
catch (Autodesk.AutoCAD.Runtime.Exception ex)
{
ed.WriteMessage(("Exception: " + ex.Message));
}
finally
{
tr.Dispose();
}
}
}
}
I have a sample on making signs on a drawing. It covers getting attributes and modifying attributes:
https://forge.autodesk.com/cloud_and_mobile/2016/02/sign-title-block-of-dwg-file-with-autocad-io-view-data-api.html
And I also have a sample on getting Table cells of a drawing:
https://forge.autodesk.com/blog/get-cell-data-autocad-table-design-automation-api
Hope these could help you to make the plugin for your requirements.
What do you mean by "Header" information? Can you give an example?
Finding an extracting all text objects is relatively easy if you are familiar with the AutoCAD .NET API (or C++ or Lisp).
Here's an example that extracts blocks and layer names:
https://github.com/Autodesk-Forge/design.automation-.net-custom.activity.sample
There is a task, using the .NET library for the Google Data API, to traverse across Google Drive folders, find required spreadsheets and change data of the selected spreadsheets.
Folders traversing is performed using the Google.GData.Documents.FolderQuery and other classes of the Google.GData.Documents namespace. After a correct document is found is necessary to manage it using the Google.GData.Spreadsheets.Spreadsheet class. Now I find a correspondence between the Google.GData.Documents.DocumentEntry class and the Google.GData.Spreadsheets.Spreadsheet class instances by extracting the document key from the document URL, iterating all spreadsheets, extracting a spreadsheet URL and comparing the two keys. The code looks like
private string GetKey(string url) {
string res = null;
Match match = Regex.Match(url, #"\?key=([A-Za-z0-9]+)");
if (match.Success) {
res = match.Groups[1].Value;
}
return res;
}
private SpreadsheetEntry GetSpreadSheetForDocument(SpreadsheetsService serviceSS, DocumentEntry entrySS) {
SpreadsheetEntry res = null;
string strSSKey = GetKey(entrySS.AlternateUri.Content);
Google.GData.Spreadsheets.SpreadsheetQuery query = new Google.GData.Spreadsheets.SpreadsheetQuery();
SpreadsheetFeed feed = serviceSS.Query(query);
foreach (SpreadsheetEntry entry in feed.Entries) {
if (GetKey(entry.AlternateUri.Content) == strSSKey) {
res = entry;
break;
}
}
return res;
}
Is there another, more elegant and correct, way to do this?
As best I can tell, not only is there no better way to do this, but even this technique will fail. As of recent(?) changes to Google Drive API, the keys for the SAME DOCUMENT retrieved by Document List versus Spreadsheets APIs are incompatible. Though using a spreadsheet URL constructed from a key returned by Document List API WILL get you a SpreadsheetEntry, spreadsheet operations on that entry are likely to produce "Invalid Token" Authentication Exceptions.
Your mileage may vary, depending on the authentication style you use. I am using the least recommended User Credentials method.
Here's another one for releasing objects created by reflection:
We're working with a reporting tool (Active Reports 6) which creates a dll for each report.
We have lots of clients that use similar but still unique reports.
Reports are read through a web interface.
We run multiple sites, one for each client.
Our choices are:
1) Put all the reports in one big project which will be called by all the sites.
Cost: It will need to be recompiled every time we make a small change to any one report, potentially creating problems for all sites.
2) Create a whole bunch of similar little projects, with one for each site - let's say for sake of space that this creates problems, too.
3) Create a "Report Factory" which will use reflection to wire-up report dlls as needed.
We chose "3".
Problem: The final product works fine except for one thing: It won't release the report dll when done.
There is not currently a problem with the operation within a test environment, but if you try to do anything in the folder with the report dlls, you get the following error message: "This action can't be completed because the folder or a file in it is open in another program"
After research on this site and others, we realized that we needed an AppDomain for each call which can be cleanly unloaded.
After still having problems, we realized that the AppDomainSetup object needed to have a setting that allowed it to optimize for multiple users (LoaderOptimization.MultiDomain)
That didn't work.
Unfortunately, the base object (Active 6 report) can not be serialized, so we can't make a deep copy and chuck the original object.
After doing all of this, we're still experiencing problems.
Here is the code (C#):
private object WireUpReport(ReportArgs args)
{
//The parameter 'args' is a custom type (ReportArgs) which merely contains a
name/value pair collection.
object myReport = null;
string sPath = String.Empty;
string sFriendlyName = String.Empty;
sFriendlyName = System.Guid.NewGuid().ToString();
Assembly asmReport = null;
AppDomainSetup ads = null;
AppDomain adWireUp = null;
ConstructorInfo ci = null;
Type myReportType = null;
Type[] parametypes = null;
object[] paramarray = null;
object retObject = null;
try
{
//Get Report Object
sPath = GetWireUpPath(args); //Gets the path to the required dll; kept in a config file
//This parameter is used in an overloaded constructor further down
ads = new AppDomainSetup();
ads.ApplicationBase = Path.GetDirectoryName(sPath);
ads.LoaderOptimization = LoaderOptimization.MultiDomain;
adWireUp = AppDomain.CreateDomain(sFriendlyName, AppDomain.CurrentDomain.Evidence, ads);
asmReport = adWireUp.GetAssemblies()[0];
asmReport = Assembly.LoadFrom(sPath);
//Create parameters for wireup
myReportType = asmReport.GetExportedTypes()[0];
parametypes = new Type[1];
parametypes[0] = typeof(ReportArgs);
ci = myReportType.GetConstructor(parametypes);
paramarray = new object[1];
paramarray[0] = args;
//Instantiate object
myReport = ci.Invoke(paramarray);
return myReport;
}
catch (Exception ex)
{
throw ex;
}
finally
{
//Make sure Assembly object is released.
if (adWireUp != null)
{
AppDomain.Unload(adWireUp);
}
if (asmReport != null)
{
asmReport = null;
}
if (ads != null)
{
ads = null;
}
if (adWireUp != null)
{
adWireUp = null;
}
if (ci != null)
{
ci = null;
}
if (myReportType != null)
{
myReportType = null;
}
if (parametypes != null)
{
parametypes = null;
}
if (paramarray != null)
{
paramarray = null;
}
}
}
The object which is returned from this code is cast as type ActiveReports and then passed around our application.
Any help would be deeply appreciated. Thanks
Your code looks like you are seriously misunderstanding how to interact with a separate AppDomain.
Think of communicating with an AppDomain like talking to someone who's currently in another country. You know where they are, but you can't just walk over and talk to them. If you want them to do something for you, you have to open up a line of communication and tell them what you need.
The way that you open that line of communication is by defining a proxy object that can be created inside the other AppDomain and then cross the boundary back to your current AppDomain. Being able to cross the boundary requires that your object either be marked as [Serializable] or inherit from MarshalByRefObject. Because we actually want to talk to a reference in the other AppDomain and not just have a copy of it, we need the proxy to do the latter.
private class CrossDomainQuery : MarshalByRefObject
{
public void LoadDataFromAssembly(string assemblyPath)
{
var assembly = Assembly.LoadFrom(assemblyPath);
//TODO: Do something with your assembly
}
}
There is a method on the AppDomain called CreateInstanceAndUnwrap() that will create an instance of that communication object inside the other AppDomain and then hand you back a __TransparentProxy object that can be cast to the proxy type.
var crossDomainQuery = (CrossDomainQuery)adWireUp.CreateInstanceAndUnwrap(
typeof(CrossDomainQuery).Assembly.FullName,
typeof(CrossDomainQuery).FullName);
Once you have that proxy object, you can call methods on it and they will be invoked in the other AppDomain.
crossDomainQuery.LoadDataFromAssembly(assemblyPath);
So how is this different from what your current example code is doing?
Your current code does not actually execute anything useful inside the other AppDomain.
adWireUp = AppDomain.CreateDomain(sFriendlyName, AppDomain.CurrentDomain.Evidence, ads);
asmReport = adWireUp.GetAssemblies()[0];
asmReport = Assembly.LoadFrom(sPath);
This creates a new AppDomain, but then it loads all of the assemblies from that AppDomain into your current AppDomain. Additionally, it explicitly loads your report assembly into your current AppDomain.
Creating an AppDomain and calling methods on it doesn't mean that your code is executing inside of it any more than reading about another country means that you're now talking to someone inside it.
Even if you do create a proxy object and execute code inside that other AppDomain, there are a few things to be aware of.
1) Both AppDomains must be able to see the type used for the proxy, and you may have to handle AssemblyResolve events for either AppDomain manually (at least temporarily) to help resolve that.
2) AppDomains are fairly expensive to create. Generally, they are not used in situations where you need to spin something up really quickly, take some action and disappear. You should plan on either keeping them around as long as you can or be prepared to take the performance hit on every call.
3) You've said that the report type that you're instantiating is not serializable, and being able to serialize the object is a requirement for passing that type back from the other AppDomain. Defining a serializable class that can transport relevant data across the boundary and using that to pass the report data might be an option, but you'll have to determine if that works for your particular situation.
Also, as an aside, unless you have logic that depends on variables being set to null, setting everything to null in your finally does nothing useful and complicates your code.