I try to add some new property in existing category for selected NavisWorks ModelItem`s
There is not so many example over network, and it base on same COM approach.
However there special method to add property available in API.
Only issue that objects is locked.
Is there any way to unlock it?
using ANA = Autodesk.Navisworks.Api;
...
private void addProperty(string category, string prop, string value)
{
var oDoc = Autodesk.Navisworks.Api.Application.ActiveDocument;
ModelItemCollection selectionModelItems = new ModelItemCollection();
ANA.Application.ActiveDocument.CurrentSelection.SelectedItems.CopyTo(selectionModelItems);
//Clear the current selection
ANA.Application.ActiveDocument.CurrentSelection.Clear();
try
{
foreach (ModelItem m in selectionModelItems)
{
foreach (PropertyCategory p in m.PropertyCategories)
{
if (p.DisplayName != category) continue;
var property = new DataProperty(prop, prop, new VariantData(value));
p.Properties.Add(property);
}
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
result of execution:
Properties and categories which are created by Navisworks are read-only.
You can not add or modify properties or categories which are created by Navisworks.
You can only create or add user-defined properties - using COM.
See:
https://adndevblog.typepad.com/aec/2012/05/create-attributes-and-properties-for-model-objects-using-net-api.html
https://adndevblog.typepad.com/aec/2012/08/addmodifyremove-custom-attribute-using-com-api.html
https://forums.autodesk.com/t5/navisworks-api/navisworks-api-add-user-data-tab/td-p/2916866
Here is a code snippet (copied from xiaodong.liang forum post mentioned above) which shows how to add a user-defined property using COM:
private void addProperty() {
ComApi.InwOpState10 state;
state = ComApiBridge.ComApiBridge.State;
ModelItemCollection modelItemCollectionIn = new ModelItemCollection(Autodesk.Navisworks.Api.Application.ActiveDocument.CurrentSelection.SelectedItems);
ComApi.InwOpSelection comSelectionOut =
ComApiBridge.ComApiBridge.ToInwOpSelection(modelItemCollectionIn);
ComApi.InwSelectionPathsColl oPaths = comSelectionOut.Paths();
ComApi.InwOaPath3 oPath = (ComApi.InwOaPath3) oPaths.Last();
ComApi.InwGUIPropertyNode2 propn = (ComApi.InwGUIPropertyNode2) state.GetGUIPropertyNode(oPath, true);
ComApi.InwOaPropertyVec newPvec = (ComApi.InwOaPropertyVec) state.ObjectFactory(Autodesk.Navisworks.Api.Interop.ComApi.nwEObjectType.eObjectType_nwOaPropertyVec, null, null);
ComApi.InwOaProperty newP = (ComApi.InwOaProperty) state.ObjectFactory(Autodesk.Navisworks.Api.Interop.ComApi.nwEObjectType.eObjectType_nwOaProperty, null, null);
newP.name = "LXD_Property_Name";
newP.UserName = "LXD_Property_UserName";
newP.value = "LXD_Property_Value";
newPvec.Properties().Add(newP);
propn.SetUserDefined(0, "LXD_PropertyTab_Name", "LXD_PropertyTab_InteralName", newPvec);
}
Related
I'm creating a Revit plugin that reads and writes modelinformation to a database, and it all works fine in debug mode, but when I release the project and run Revit with the plugin outside visual studio, I'm getting an error when the plugin tries to read data from the database.
The code runs on DocumenetOpened event and looks like this:
public void application_DocumentOpenedEvent(object sender, DocumentOpenedEventArgs e)
{
UIApplication uiapp = new UIApplication(sender as Autodesk.Revit.ApplicationServices.Application);
Document doc = uiapp.ActiveUIDocument.Document;
//ModelGUID COMMAND
var command = new ModelCheckerCommandExec();
command.Execute(uiapp);
}
It then fails on the following line:
ModelsList = (DatabaseHelper.ReadNonAsync<RevitModel>())
.Where(m => m.ModelGUID == DataStores.ModelData.ModelGUID).ToList();
In this code block that gets executed:
public class ModelCheckerCommandExec : IExternalCommand
{
public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
{
return Execute(commandData.Application);
}
public Result Execute(UIApplication uiapp)
{
Document doc = uiapp.ActiveUIDocument.Document;
Transaction trans = new Transaction(doc);
try
{
trans.Start("ModelGUID");
ModelGUIDCommand.GetAndSetGUID(doc);
trans.Commit();
var ModelsList = new List<RevitModel>();
ModelsList = (DatabaseHelper.ReadNonAsync<RevitModel>()).ToList();//.Where(m => m.ModelGUID == DataStores.ModelData.ModelGUID).ToList(); // Read method only finds models the are similar to the DataStore.ModelDate.DBId;
if (ModelsList.Count == 1)
{
trans.Start("DataFromDB");
doc.ProjectInformation.Name = ModelsList[0].ProjectName;
doc.ProjectInformation.Number = ModelsList[0].ModelNumber;
doc.ProjectInformation.Status = ModelsList[0].ModelStatus;
doc.ProjectInformation.IssueDate = ModelsList[0].ProjectIssueDate;
doc.ProjectInformation.ClientName = ModelsList[0].ClientName;
doc.ProjectInformation.Address = ModelsList[0].ProjectAddress;
doc.ProjectInformation.LookupParameter("Cadastral Data").Set(ModelsList[0].ProjectIssueDate);
doc.ProjectInformation.LookupParameter("Current Version").Set(ModelsList[0].CurrentVersion);
doc.ProjectInformation.BuildingName = ModelsList[0].BuildingName;
DataStores.ModelData.ModelManager1 = ModelsList[0].ModelManagerOne;
DataStores.ModelData.ModelManager1Id = ModelsList[0].ModelManagerOneId;
DataStores.ModelData.ModelManager2 = ModelsList[0].ModelManagerTwo;
DataStores.ModelData.ModelManager2Id = ModelsList[0].ModelManagerTwoId;
trans.Commit();
}
return Result.Succeeded;
}
catch (Exception ex)
{
TaskDialog.Show("Error", ex.Message);
return Result.Failed;
}
}
}
The "ReadNonAsync" method is as follows:
public static List<T> ReadNonAsync<T>() where T : IHasId
{
using (var client = new HttpClient())
{
var result = client.GetAsync($"{dbPath}{Properties.Settings.Default.CompanyName}_{typeof(T).Name.ToLower()}.json?access_token={DataStores.IdToken.UserIdToken}").GetAwaiter().GetResult();
var jsonResult = result.Content.ReadAsStringAsync().GetAwaiter().GetResult();
if (result.IsSuccessStatusCode)
{
var objects = JsonConvert.DeserializeObject<Dictionary<string, T>>(jsonResult);
List<T> list = new List<T>();
if (objects != null)
{
foreach (var o in objects)
{
o.Value.Id = o.Key;
list.Add(o.Value);
}
}
return list;
}
else
{
return null;
}
}
}
In the rest of my code I use a async Read method which works, so I'm wondering wether or not that's the issue, but Revit wont let me use an async method inside an Execute method.
How do I debug this issue correctly, and why could there be code working in debug that doesn't work in "live" versions?
I found a solution!
The issue:
The reason for the error was that when I run the software in debug-mode, a file path of "xxx.txt" finds files in the solution folder, but when I run the software "xxx.txt" points to the folder of the software and not the .dll -
So in my case it pointed to "c:\Program Files\Autodesk\Revit\2021".
The fix:
Hard coding the path, or by getting the path of the executing .dll
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)
Debugging/Troubleshooting:
I found the issue by inserting dialogboxes with errormessages in all my try/catch statements.
I am creating a designer surface and loading the controls to a runtime.
I am having issues when deserializing/loading the controls to the runtime.
All methods I have tried seem to have some type of issue.
Issued faced for example:
Controls are still bound of the design-time
Not all properties deserialize with all the properties, namely nested properties.
Control associations does seem to be followed, i.e. Button in a Panel, will not be in the panel anymore, even though the property is still the parent after loading.
I have created a sample Project on git here: Surface Designer Test
There are the main code snippets:
Serialization from the design-time
private void LoadRuntime(int type)
{
var controls = surface.ComponentContainer.Components;
SerializationStore data = (SerializationStore)surface.
_designerSerializationService.Serialize(controls);
MemoryStream ms = new MemoryStream();
data.Save(ms);
SaveData.Data = ms.ToArray();
SaveData.LoadType = type;
new RuntimeForm().Show();
}
public object Serialize(System.Collections.ICollection objects)
{
ComponentSerializationService componentSerializationService =
_serviceProvider.GetService(typeof(ComponentSerializationService)) as
ComponentSerializationService;
SerializationStore returnObject = null;
using (SerializationStore serializationStore =
componentSerializationService.CreateStore())
{
foreach (object obj in objects)
{
if (obj is Control control)
{
componentSerializationService.SerializeAbsolute(serializationStore, obj);
}
returnObject = serializationStore;
}
}
return returnObject;
}
Deserialization in runtime
Here is attempt with reflection:
MemoryStream ms = new MemoryStream(SaveData.Data);
Designer d = new Designer();
var controls = d._designerSerializationService.Deserialize(ms);
ms.Close();
if (SaveData.LoadType == 1)
{
foreach (Control cont in controls)
{
var ts = Assembly.Load(cont.GetType().Assembly.FullName);
var o = ts.GetType(cont.GetType().FullName);
Control controlform = (Control)Activator.CreateInstance(o);
PropertyInfo[] controlProperties = cont.GetType().GetProperties();
foreach (PropertyInfo propInfo in controlProperties)
{
if (propInfo.CanWrite)
{
if (propInfo.Name != "Site" && propInfo.Name != WindowTarget")
{
try
{
var obj = propInfo.GetValue(cont, null);
propInfo.SetValue(controlform, obj, null);
}
catch { }
}
else { }
}
}
Controls.Add(controlform);
}
}
Here is attempt with loading controls directly (still bound to the design-time):
MemoryStream ms = new MemoryStream(SaveData.Data);
Designer d = new Designer();
var controls = d._designerSerializationService.Deserialize(ms);
foreach (Control cont in controls)
Controls.Add(cont);
I feel like I am missing a concept from the System.ComponentModel.Design framework.
I also do not believe there is a need to write a custom serializer for each control, as surely the already have this has Visual Studio is able to serialize all their properties as they are changed in the PropertyGrid and load them back when you run the program.
I'd love to serialize the designer into a .cs file, but how? How do you serialize controls/form and changed properties to a file like the VS designer, I tried and looked only to find xml and binary serializer. My ideal solution would be build a designer.cs with the CodeDom.
What is the correct way do accomplish this serialization between design-time and run-time?
Assuming you have a DesignSurface to show a Form as root component of the designer and having some components created at run-time by using CreateComponent method of IDesignerHost, here is how I approach the problem:
Get an instance of IDesignerHost from DesignSurface
Create new DesignerSerializationManager
Get an instance of TypeCodeDomSerializer from serialization manager
Serialize the RootComponent of the IDesignerHost
Create an instance of CSharpCodeProvider
Generate code by calling GenerateCodeFromType and passing the serialized root component.
You can also extend the example a bit and use ISelectionService to get notified about selected components and change properties at run-time using a PropertyGrid:
Example - Generate C# code from DesignSurface at runtime
Here in this example, I'll show how you can host a windows forms designer at run-time and design a form containing some controls and components and generate C# code at run-time and run the generated code.
Please note: It's not a production code and it's just an example as a
proof of concept.
Create the DesignSurface and host the designer
You can create the design surface like this:
DesignSurface designSurface;
private void Form1_Load(object sender, EventArgs e)
{
designSurface = new DesignSurface(typeof(Form));
var host = (IDesignerHost)designSurface.GetService(typeof(IDesignerHost));
var root = (Form)host.RootComponent;
TypeDescriptor.GetProperties(root)["Name"].SetValue(root, "Form1");
root.Text = "Form1";
var button1 = (Button)host.CreateComponent(typeof(Button), "button1");
button1.Text = "button1";
button1.Location = new Point(8, 8);
root.Controls.Add(button1);
var timer1 = (Timer)host.CreateComponent(typeof(Timer), "timer1");
timer1.Interval = 2000;
var view = (Control)designSurface.View;
view.Dock = DockStyle.Fill;
view.BackColor = Color.White;
this.Controls.Add(view);
}
Generate C# code using TypeCodeDomSerializer and CSharpCodeProvider
This is how I generate code from design surface:
string GenerateCSFromDesigner(DesignSurface designSurface)
{
CodeTypeDeclaration type;
var host = (IDesignerHost)designSurface.GetService(typeof(IDesignerHost));
var root = host.RootComponent;
var manager = new DesignerSerializationManager(host);
using (manager.CreateSession())
{
var serializer = (TypeCodeDomSerializer)manager.GetSerializer(root.GetType(),
typeof(TypeCodeDomSerializer));
type = serializer.Serialize(manager, root, host.Container.Components);
type.IsPartial = true;
type.Members.OfType<CodeConstructor>()
.FirstOrDefault().Attributes = MemberAttributes.Public;
}
var builder = new StringBuilder();
CodeGeneratorOptions option = new CodeGeneratorOptions();
option.BracingStyle = "C";
option.BlankLinesBetweenMembers = false;
using (var writer = new StringWriter(builder, CultureInfo.InvariantCulture))
{
using (var codeDomProvider = new CSharpCodeProvider())
{
codeDomProvider.GenerateCodeFromType(type, writer, option);
}
return builder.ToString();
}
}
For example:
var code = GenerateCSFromDesigner(designSurface);
Run the code sing CSharpCodeProvider
Then to run it:
void Run(string code, string formName)
{
var csc = new CSharpCodeProvider();
var parameters = new CompilerParameters(new[] {
"mscorlib.dll",
"System.Windows.Forms.dll",
"System.dll",
"System.Drawing.dll",
"System.Core.dll",
"Microsoft.CSharp.dll"});
parameters.GenerateExecutable = true;
code = $#"
{code}
public class Program
{{
[System.STAThread]
static void Main()
{{
System.Windows.Forms.Application.EnableVisualStyles();
System.Windows.Forms.Application.SetCompatibleTextRenderingDefault(false);
System.Windows.Forms.Application.Run(new {formName}());
}}
}}";
var results = csc.CompileAssemblyFromSource(parameters, code);
if (!results.Errors.HasErrors)
{
System.Diagnostics.Process.Start(results.CompiledAssembly.CodeBase);
}
else
{
var errors = string.Join(Environment.NewLine,
results.Errors.Cast<CompilerError>().Select(x => x.ErrorText));
MessageBox.Show(errors);
}
}
For example:
Run(GenerateCSFromDesigner(designSurface), "Form1");
When I update my member, I want to update his BankCheck too.
This is my database:
My bankCheck can be added, updated or deleted.
My member can be updated only (name, surname...)
I choose my member in my datagrid, and select Edit, my wpf app switch to an other page and display my member with Textbox etc.
I click on my button to add/edit/delete his bankCheck and I can edit the first bankCheck.
I delete the last bankCheck and I add an other (for example).
I press OK and I click on "Valid my Edit".
My program re-creates a new Member with his bankCheck and i made this:
private void EditMember(Member updatedMember)
{
try
{
using (var context = new KravMagaEntities())
{
context.Member.Attach(updatedMember);
context.Entry(updatedMember).State = EntityState.Modified;
context.SaveChanges();
}
ResetAllControls();
States.EnumToText(States.StatesEnum.UpdatingSuccess);
Application.Current.Dispatcher.Invoke(() =>
{
_managementService.IsVisibleAddTab(true);
_managementService.IsVisibleEditTab(false);
});
}
catch (Exception exception)
{
States.EnumToText(States.StatesEnum.Error, exception);
}
}
But I have this error:
A referential integrity constraint violation occurred: The property values that define the referential constraints are not consistent between principal and dependent objects in the relationship.
I don't know how I can fix this error.
Thank you.
My code:
private void OnEditMemberBtnClicked(object sender, RoutedEventArgs e)
{
try
{
var isValidateCertificat = IsValidDate(BirthDateTxt);
var isValidateBirth = IsValidDate(CertificateDateTxt);
var isValidateAutorisation = IsValidDate(AutorizationDateTxt);
var isValidateReglement = IsValidDate(RuleDateTxt);
if (isValidateBirth && isValidateCertificat && isValidateAutorisation && isValidateReglement)
{
States.EnumToText(States.StatesEnum.Updating);
var typePaiement = BankCheckRadio.IsChecked.Value;
var typePaiementText = typePaiement ? "Chèque" : "Espèce";
var doctor = "";
var dateCertificate = "";
if (BankCheckRadio.IsChecked.Value)
{
doctor = DoctorTxt.Text;
dateCertificate = CertificateDateTxt.Text;
}
var editedMember = new Member
{
id_Member = _idForEdit,
name_Member = UppercaseChar(NameTxt.Text),
surname_Member = UppercaseChar(SurnameTxt.Text),
birthDate_Member = BirthDateTxt.Text,
autorizationDate_Member = AutorizationDateTxt.Text,
address_Member = UppercaseChar(AddressTxt.Text),
postalCode_Member = PostalCodeTxt.Text,
country_Member = UppercaseChar(CountryTxt.Text),
fixPhone_Member = FixPhoneTxt.Text,
mobilePhone_Member = MobilePhoneTxt.Text,
mail_Member = MailTxt.Text,
beginDate_Member = BeginDateCombo.Text,
ruleDate_Member = RuleDateTxt.Text,
subscription_Member = SubscriptionCombo.Text,
typePaiement_Member = typePaiement,
typePaiementText_Member = typePaiementText,
federationNumero_Member = FederationNumeroTxt.Text.ToUpper(),
level_Member = LevelCombo.Text,
certificate_Member = CertificateCheckbox.IsChecked.Value,
doctor_Member = UppercaseChar(doctor),
certificateDate_Member = dateCertificate,
problem_Member = UppercaseChar(ProblemTxt.Text, true),
emergencyName_Member = UppercaseChar(EmergencyNameTxt.Text),
emergencyPhone_Member = EmergencyPhoneTxt.Text,
BankCheck = _bankChecks
};
if (_bankChecks != null)
{
using (var context = new KravMagaEntities())
{
foreach (var bankCheck in _bankChecks)
{
bankCheck.idMember_BankCheck = editedMember.id_Member;
context.Entry(bankCheck).State = EntityState.Added;
}
context.SaveChanges();
}
}
new Task(() => EditMember(editedMember)).Start();
}
}
catch (Exception exception)
{
States.EnumToText(States.StatesEnum.Error, exception);
}
}
So as I see, you're updating only Member, not all the modified BankAccounts. You're updating navigation property of entities from both sides but calling SaveChanges() only on entity from one side. So your Member starts to refer another BankAccount while your BankAccounts still refer to the old Member. You need to mark all appropriate BankAccounts as modified along with your modified Member in the same place and then call SaveChanges() so everything will be saved (from comment).
To prevent adding a duplicate you can try to set the state of your entities to the State.Modified instead of State.Added.
The reason of that problem was that you were updating only entity from one side. If you have BankAccounts-Members relationship then in case you update the navtigation property for Member you should update a navigation property for BankAccount too and vice versa. If you just update some property (Member.Name or anything) you just set this Member's State to State.Modified without affecting any of other Member's, BankAccount's etc.
If the entity tracking is turned on for you then EF will automatically track entities that were modified and set appropriate states for them. But as I've seen from your issue, it's turned off for you so you have to manually set the state for each object you want to add/update/delete.
So what I really want is somewhat usable tab completion in a PS module.
ValidateSet seems to be the way to go here.
Unfortunately my data is dynamic, so I cannot annotate the parameter with all valid values upfront.
DynamicParameters/IDynamicParameters seems to be the solution for that problem.
Putting these things together (and reducing my failure to a simple test case) we end up with:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Management.Automation;
using System.Text;
using System.Threading.Tasks;
namespace PSDummy
{
[Cmdlet(VerbsCommon.Get, "BookDetails")]
public class GetBookDetails : Cmdlet, IDynamicParameters
{
IDictionary<string, string[]> m_dummyData = new Dictionary<string, string[]> {
{"Terry Pratchett", new [] {"Small Gods", "Mort", "Eric"}},
{"Douglas Adams", new [] {"Hitchhiker's Guide", "The Meaning of Liff"}}
};
private RuntimeDefinedParameter m_authorParameter;
private RuntimeDefinedParameter m_bookParameter;
protected override void ProcessRecord()
{
// Do stuff here..
}
public object GetDynamicParameters()
{
var parameters = new RuntimeDefinedParameterDictionary();
m_authorParameter = CreateAuthorParameter();
m_bookParameter = CreateBookParameter();
parameters.Add(m_authorParameter.Name, m_authorParameter);
parameters.Add(m_bookParameter.Name, m_bookParameter);
return parameters;
}
private RuntimeDefinedParameter CreateAuthorParameter()
{
var p = new RuntimeDefinedParameter(
"Author",
typeof(string),
new Collection<Attribute>
{
new ParameterAttribute {
ParameterSetName = "BookStuff",
Position = 0,
Mandatory = true
},
new ValidateSetAttribute(m_dummyData.Keys.ToArray()),
new ValidateNotNullOrEmptyAttribute()
});
// Actually this is always mandatory, but sometimes I can fall back to a default
// value. How? p.Value = mydefault?
return p;
}
private RuntimeDefinedParameter CreateBookParameter()
{
// How to define a ValidateSet based on the parameter value for
// author?
var p = new RuntimeDefinedParameter(
"Book",
typeof(string),
new Collection<Attribute>
{
new ParameterAttribute {
ParameterSetName = "BookStuff",
Position = 1,
Mandatory = true
},
new ValidateSetAttribute(new string[1] { string.Empty }/* cannot fill this, because I cannot access the author */),
new ValidateNotNullOrEmptyAttribute()
});
return p;
}
}
}
Unfortunately this tiny snippet causes a lot of issues already. Ordered descending:
I fail to see how I can create a connection between the parameters. If you pick an author, you should only be able to pick a book that matches the author. So far GetDynamicParameters() always seems stateless though: I see no way to access the value of a different/earlier dynamic parameter. Tried keeping it in a field, tried searching MyInvocation - no luck. Is that even possible?
How do you define a default value for mandatory parameter? Doesn't fit the silly example, but let's say you can store your favorite author. From now on I want to default to that author, but having a pointer to an author is still mandatory. Either you gave me a default (and can still specify something else) or you need to be explicit.
Tab completion for strings with spaces seems weird/broken/limited - because it doesn't enclose the value with quotes (like cmd.exe would do, for example, if you type dir C:\Program <tab>). So tab completion actually breaks the invocation (if the issues above would be resolved, Get-BookDetails Ter<tab> would/will expand to Get-BookDetails Terry Pratchett which puts the last name in parameter position 1 aka 'book'.
Shouldn't be so hard, surely someone did something similar already?
Update: After another good day of tinkering and fooling around I don't see a way to make this work. The commandlet is stateless and will be instantiated over and over again. At the point in time when I can define dynamic parameters (GetDynamicParameters) I cannot access their (current) values/see what they'd be bound to - e.g. MyInvocation.BoundParameters is zero. I'll leave the question open, but it seems as if this just isn't supported. All the examples I see add a dynamic parameter based on the value of a static one - and that's not relevant here. Bugger.
I think this works. Unfortunately, it uses reflection to get at some of the cmdlet's private members for your first bullet. I got the idea from Garrett Serack. I'm not sure if I completely understood how to do the default author, so I made it so that the last valid author is stored in a static field so you don't need -Author the next time.
Here's the code:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Management.Automation;
using System.Text;
using System.Threading.Tasks;
namespace PSDummy
{
internal class DynParamQuotedString {
/*
This works around the PowerShell bug where ValidateSet values aren't quoted when necessary, and
adding the quotes breaks it. Example:
ValidateSet valid values = 'Test string' (The quotes are part of the string)
PowerShell parameter binding would interperet that as [Test string] (no single quotes), which wouldn't match
the valid value (which has the quotes). If you make the parameter a DynParamQuotedString, though,
the parameter binder will coerce [Test string] into an instance of DynParamQuotedString, and the binder will
call ToString() on the object, which will add the quotes back in.
*/
internal static string DefaultQuoteCharacter = "'";
public DynParamQuotedString(string quotedString) : this(quotedString, DefaultQuoteCharacter) {}
public DynParamQuotedString(string quotedString, string quoteCharacter) {
OriginalString = quotedString;
_quoteCharacter = quoteCharacter;
}
public string OriginalString { get; set; }
string _quoteCharacter;
public override string ToString() {
// I'm sure this is missing some other characters that need to be escaped. Feel free to add more:
if (System.Text.RegularExpressions.Regex.IsMatch(OriginalString, #"\s|\(|\)|""|'")) {
return string.Format("{1}{0}{1}", OriginalString.Replace(_quoteCharacter, string.Format("{0}{0}", _quoteCharacter)), _quoteCharacter);
}
else {
return OriginalString;
}
}
public static string[] GetQuotedStrings(IEnumerable<string> values) {
var returnList = new List<string>();
foreach (string currentValue in values) {
returnList.Add((new DynParamQuotedString(currentValue)).ToString());
}
return returnList.ToArray();
}
}
[Cmdlet(VerbsCommon.Get, "BookDetails")]
public class GetBookDetails : PSCmdlet, IDynamicParameters
{
IDictionary<string, string[]> m_dummyData = new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase) {
{"Terry Pratchett", new [] {"Small Gods", "Mort", "Eric"}},
{"Douglas Adams", new [] {"Hitchhiker's Guide", "The Meaning of Liff"}},
{"An 'Author' (notice the ')", new [] {"A \"book\"", "Another 'book'","NoSpace(ButCharacterThatShouldBeEscaped)", "NoSpace'Quoted'", "NoSpace\"Quoted\""}} // Test value I added
};
protected override void ProcessRecord()
{
WriteObject(string.Format("Author = {0}", _author));
WriteObject(string.Format("Book = {0}", ((DynParamQuotedString) MyInvocation.BoundParameters["Book"]).OriginalString));
}
// Making this static means it should keep track of the last author used
static string _author;
public object GetDynamicParameters()
{
// Get 'Author' if found, otherwise get first unnamed value
string author = GetUnboundValue("Author", 0) as string;
if (!string.IsNullOrEmpty(author)) {
_author = author.Trim('\'').Replace(
string.Format("{0}{0}", DynParamQuotedString.DefaultQuoteCharacter),
DynParamQuotedString.DefaultQuoteCharacter
);
}
var parameters = new RuntimeDefinedParameterDictionary();
bool isAuthorParamMandatory = true;
if (!string.IsNullOrEmpty(_author) && m_dummyData.ContainsKey(_author)) {
isAuthorParamMandatory = false;
var m_bookParameter = new RuntimeDefinedParameter(
"Book",
typeof(DynParamQuotedString),
new Collection<Attribute>
{
new ParameterAttribute {
ParameterSetName = "BookStuff",
Position = 1,
Mandatory = true
},
new ValidateSetAttribute(DynParamQuotedString.GetQuotedStrings(m_dummyData[_author])),
new ValidateNotNullOrEmptyAttribute()
}
);
parameters.Add(m_bookParameter.Name, m_bookParameter);
}
// Create author parameter. Parameter isn't mandatory if _author
// has a valid author in it
var m_authorParameter = new RuntimeDefinedParameter(
"Author",
typeof(DynParamQuotedString),
new Collection<Attribute>
{
new ParameterAttribute {
ParameterSetName = "BookStuff",
Position = 0,
Mandatory = isAuthorParamMandatory
},
new ValidateSetAttribute(DynParamQuotedString.GetQuotedStrings(m_dummyData.Keys.ToArray())),
new ValidateNotNullOrEmptyAttribute()
}
);
parameters.Add(m_authorParameter.Name, m_authorParameter);
return parameters;
}
/*
TryGetProperty() and GetUnboundValue() are from here: https://gist.github.com/fearthecowboy/1936f841d3a81710ae87
Source created a dictionary for all unbound values; I had issues getting ValidateSet on Author parameter to work
if I used that directly for some reason, but changing it into a function to get a specific parameter seems to work
*/
object TryGetProperty(object instance, string fieldName) {
var bindingFlags = System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public;
// any access of a null object returns null.
if (instance == null || string.IsNullOrEmpty(fieldName)) {
return null;
}
var propertyInfo = instance.GetType().GetProperty(fieldName, bindingFlags);
if (propertyInfo != null) {
try {
return propertyInfo.GetValue(instance, null);
}
catch {
}
}
// maybe it's a field
var fieldInfo = instance.GetType().GetField(fieldName, bindingFlags);
if (fieldInfo!= null) {
try {
return fieldInfo.GetValue(instance);
}
catch {
}
}
// no match, return null.
return null;
}
object GetUnboundValue(string paramName) {
return GetUnboundValue(paramName, -1);
}
object GetUnboundValue(string paramName, int unnamedPosition) {
// If paramName isn't found, value at unnamedPosition will be returned instead
var context = TryGetProperty(this, "Context");
var processor = TryGetProperty(context, "CurrentCommandProcessor");
var parameterBinder = TryGetProperty(processor, "CmdletParameterBinderController");
var args = TryGetProperty(parameterBinder, "UnboundArguments") as System.Collections.IEnumerable;
if (args != null) {
var currentParameterName = string.Empty;
object unnamedValue = null;
int i = 0;
foreach (var arg in args) {
var isParameterName = TryGetProperty(arg, "ParameterNameSpecified");
if (isParameterName != null && true.Equals(isParameterName)) {
string parameterName = TryGetProperty(arg, "ParameterName") as string;
currentParameterName = parameterName;
continue;
}
// Treat as a value:
var parameterValue = TryGetProperty(arg, "ArgumentValue");
if (currentParameterName != string.Empty) {
// Found currentParameterName's value. If it matches paramName, return
// it
if (currentParameterName.Equals(paramName, StringComparison.OrdinalIgnoreCase)) {
return parameterValue;
}
}
else if (i++ == unnamedPosition) {
unnamedValue = parameterValue; // Save this for later in case paramName isn't found
}
// Found a value, so currentParameterName needs to be cleared
currentParameterName = string.Empty;
}
if (unnamedValue != null) {
return unnamedValue;
}
}
return null;
}
}
}
I am playing around with a SharePoint server and I am trying to programmatically add a service request to microsoft's call center application template. So far, I have had pretty good success. I can add a call for a specified customer and assign a specific support tech:
private enum FieldNames
{
[EnumExtension.Value("Service Request")]
ServiceRequest,
[EnumExtension.Value("Customer")]
Customer,
[EnumExtension.Value("Service Representative")]
ServiceRepresentative,
[EnumExtension.Value("Assigned To")]
AssignedTo,
[EnumExtension.Value("Software")]
Software,
[EnumExtension.Value("Category")]
Category
}
private void CreateServiceCall(string serviceCallTitle, string customerName, string serviceRep)
{
SPSite allSites = new SPSite(siteURL);
SPWeb site = allSites.AllWebs[siteName];
SPListItemCollection requestsList = site.Lists[serviceRequests].Items;
SPListItem item = requestsList.Add();
SPFieldLookup customerLookup = item.Fields[FieldNames.Customer.Value()] as SPFieldLookup;
item[FieldNames.ServiceRequest.Value()] = serviceCallTitle;
if (customerLookup != null)
{
using (SPWeb lookupWeb = allSites.OpenWeb(customerLookup.LookupWebId))
{
SPList lookupList = lookupWeb.Lists.GetList(new Guid(customerLookup.LookupList), false);
foreach (SPListItem listItem in lookupList.Items)
{
if (listItem[customerLookup.LookupField].ToString() != customerName) continue;
item[FieldNames.Customer.Value()] = new SPFieldLookupValue(listItem.ID, customerName);
break;
}
}
}
SPUserCollection userCollection = site.SiteUsers;
if (userCollection != null)
{
foreach (SPUser user in userCollection)
{
if (user.Name != serviceRep) continue;
item[FieldNames.AssignedTo.Value()] = user;
break;
}
}
item.Update();
site.Close();
allSites.Close();
}
I added two custom columns (category, software) to the default list:
I populated both of these columns inside of SharePoint, now I want to retrieve that data so I can use it in the code snippet I posted to assign the proper category/software etc to the call. I have not been able to get the list in the code, I have tried using a item["Software"], site.Lists["Software"] and a couple of others, but so far all I have come up is null.
Can anyone point me in the right direction for this? Thanks!
SPFieldMultiChoice and related fields have a Choices property:
SPFieldMultiChoice software = item.Fields[FieldNames.Software.Value()] as SPFieldMultiChoice;
StringCollection softwareChoices = software.Choices;
If you need to set a value on the field, use the SPFieldMultiChoiceValue type:
SPFieldMultiChoiceValue values = new SPFieldMultiChoiceValue();
values.Add("Choice 1");
values.Add("Choice 2");
item[FieldNames.Software.Value()] = values;