How to use HIGH_COMPRESSION in Lucene.Net 4.8 - lucene

I'm trying to compress the index size as much as possible, Any help please?
https://lucenenet.apache.org/docs/4.8.0-beta00013/api/core/Lucene.Net.Codecs.Compressing.CompressionMode.html#Lucene_Net_Codecs_Compressing_CompressionMode_HIGH_COMPRESSION
public class LuceneIndexer
{
private Analyzer _analyzer = new ArabicAnalyzer(Lucene.Net.Util.LuceneVersion.LUCENE_48);
private string _indexPath;
private Directory _indexDirectory;
public IndexWriter _indexWriter;
public LuceneIndexer(string indexPath)
{
this._indexPath = indexPath;
_indexDirectory = new SimpleFSDirectory(new System.IO.DirectoryInfo(_indexPath));
}
public void BuildCompleteIndex(IEnumerable<Document> documents)
{
IndexWriterConfig indexWriterConfig = new IndexWriterConfig(Lucene.Net.Util.LuceneVersion.LUCENE_48, _analyzer) { OpenMode = OpenMode.CREATE_OR_APPEND };
indexWriterConfig.MaxBufferedDocs = 2;
indexWriterConfig.RAMBufferSizeMB = 128;
indexWriterConfig.MaxThreadStates = 2;
_indexWriter = new IndexWriter(_indexDirectory, indexWriterConfig);
_indexWriter.AddDocuments(documents);
_indexWriter.Flush(true, true);
_indexWriter.Commit();
_indexWriter.Dispose();
}
public IEnumerable<Document> Search(string searchTerm, string searchField, int limit)
{
IndexReader indexReader = DirectoryReader.Open(_indexDirectory);
var searcher = new IndexSearcher(indexReader);
var termQuery = new TermQuery(new Term(searchField, searchTerm)); // Lucene.Net.Util.LuceneVersion.LUCENE_48, searchField, _analyzer
var hits = searcher.Search(termQuery, limit).ScoreDocs;
var documents = new List<Document>();
foreach (var hit in hits)
{
documents.Add(searcher.Doc(hit.Doc));
}
_analyzer.Dispose();
return documents;
}
}

The first thing to know is that there are many aspects to the "Lucene Index". When not using compound files, this manifests in the various files that are created. Just looking at two of those, we can talk about the inverted index which is called postings and we can talk about the stored documents. Of these two, there aren't any readily available tunable settings regarding the compression of the inverted index as best I can tell.
The HIGH_COMPRESSION mode relates to the stored fields. If you are not storing fields and you are only using Lucene.Net to create an inverted index then doing work to turn on high compression for stored fields won't reduce the size of the "Lucene Index".
That said, if you are storing fields and want to use high compression on that stored fields data, then you will need to create your own codec that has high compression turned on for stored fields. And to do that, you will first need a Stored fields class that has high compression turned on. Below are those two classes followed by a unit test that uses this new codec that I have written for you. I haven't tried this code on a large amount of data to see the effect, I leave that for you as an exercise, but this should point the way to getting your stored fields compressed with High Compression.
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
public sealed class Lucene41StoredFieldsHighCompressionFormat : CompressingStoredFieldsFormat {
/// <summary>
/// Sole constructor. </summary>
public Lucene41StoredFieldsHighCompressionFormat()
: base("Lucene41StoredFieldsHighCompression", CompressionMode.HIGH_COMPRESSION, 1 << 14) {
}
}
Here is a custom codec to use this High Compression format:
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using Lucene40LiveDocsFormat = Lucene.Net.Codecs.Lucene40.Lucene40LiveDocsFormat;
using Lucene41StoredFieldsFormat = Lucene.Net.Codecs.Lucene41.Lucene41StoredFieldsFormat;
using Lucene42NormsFormat = Lucene.Net.Codecs.Lucene42.Lucene42NormsFormat;
using Lucene42TermVectorsFormat = Lucene.Net.Codecs.Lucene42.Lucene42TermVectorsFormat;
using PerFieldDocValuesFormat = Lucene.Net.Codecs.PerField.PerFieldDocValuesFormat;
using PerFieldPostingsFormat = Lucene.Net.Codecs.PerField.PerFieldPostingsFormat;
/// <summary>
/// Implements the Lucene 4.6 index format, with configurable per-field postings
/// and docvalues formats.
/// <para/>
/// If you want to reuse functionality of this codec in another codec, extend
/// <see cref="FilterCodec"/>.
/// <para/>
/// See <see cref="Lucene.Net.Codecs.Lucene46"/> package documentation for file format details.
/// <para/>
/// #lucene.experimental
/// </summary>
// NOTE: if we make largish changes in a minor release, easier to just make Lucene46Codec or whatever
// if they are backwards compatible or smallish we can probably do the backwards in the postingsreader
// (it writes a minor version, etc).
[CodecName("Lucene46HighCompression")]
public class Lucene46HighCompressionCodec : Codec {
private readonly StoredFieldsFormat fieldsFormat = new Lucene41StoredFieldsHighCompressionFormat(); //<--This is the only line different then the stock Lucene46Codec
private readonly TermVectorsFormat vectorsFormat = new Lucene42TermVectorsFormat();
private readonly FieldInfosFormat fieldInfosFormat = new Lucene46FieldInfosFormat();
private readonly SegmentInfoFormat segmentInfosFormat = new Lucene46SegmentInfoFormat();
private readonly LiveDocsFormat liveDocsFormat = new Lucene40LiveDocsFormat();
private readonly PostingsFormat postingsFormat;
private class PerFieldPostingsFormatAnonymousInnerClassHelper : PerFieldPostingsFormat {
private readonly Lucene46HighCompressionCodec outerInstance;
public PerFieldPostingsFormatAnonymousInnerClassHelper(Lucene46HighCompressionCodec outerInstance) {
this.outerInstance = outerInstance;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override PostingsFormat GetPostingsFormatForField(string field) {
return outerInstance.GetPostingsFormatForField(field);
}
}
private readonly DocValuesFormat docValuesFormat;
private class PerFieldDocValuesFormatAnonymousInnerClassHelper : PerFieldDocValuesFormat {
private readonly Lucene46HighCompressionCodec outerInstance;
public PerFieldDocValuesFormatAnonymousInnerClassHelper(Lucene46HighCompressionCodec outerInstance) {
this.outerInstance = outerInstance;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override DocValuesFormat GetDocValuesFormatForField(string field) {
return outerInstance.GetDocValuesFormatForField(field);
}
}
/// <summary>
/// Sole constructor. </summary>
public Lucene46HighCompressionCodec()
: base() {
postingsFormat = new PerFieldPostingsFormatAnonymousInnerClassHelper(this);
docValuesFormat = new PerFieldDocValuesFormatAnonymousInnerClassHelper(this);
}
public override sealed StoredFieldsFormat StoredFieldsFormat => fieldsFormat;
public override sealed TermVectorsFormat TermVectorsFormat => vectorsFormat;
public override sealed PostingsFormat PostingsFormat => postingsFormat;
public override sealed FieldInfosFormat FieldInfosFormat => fieldInfosFormat;
public override sealed SegmentInfoFormat SegmentInfoFormat => segmentInfosFormat;
public override sealed LiveDocsFormat LiveDocsFormat => liveDocsFormat;
/// <summary>
/// Returns the postings format that should be used for writing
/// new segments of <paramref name="field"/>.
/// <para/>
/// The default implementation always returns "Lucene41"
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public virtual PostingsFormat GetPostingsFormatForField(string field) {
// LUCENENET specific - lazy initialize the codec to ensure we get the correct type if overridden.
if (defaultFormat == null) {
defaultFormat = Lucene.Net.Codecs.PostingsFormat.ForName("Lucene41");
}
return defaultFormat;
}
/// <summary>
/// Returns the docvalues format that should be used for writing
/// new segments of <paramref name="field"/>.
/// <para/>
/// The default implementation always returns "Lucene45"
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public virtual DocValuesFormat GetDocValuesFormatForField(string field) {
// LUCENENET specific - lazy initialize the codec to ensure we get the correct type if overridden.
if (defaultDVFormat == null) {
defaultDVFormat = Lucene.Net.Codecs.DocValuesFormat.ForName("Lucene45");
}
return defaultDVFormat;
}
public override sealed DocValuesFormat DocValuesFormat => docValuesFormat;
// LUCENENET specific - lazy initialize the codecs to ensure we get the correct type if overridden.
private PostingsFormat defaultFormat;
private DocValuesFormat defaultDVFormat;
private readonly NormsFormat normsFormat = new Lucene42NormsFormat();
public override sealed NormsFormat NormsFormat => normsFormat;
}
Thanks to #NightOwl888, I now understand that you will also need to register the new Codec at startup like so:
Codec.SetCodecFactory(new DefaultCodecFactory {
CustomCodecTypes = new Type[] { typeof(Lucene46HighCompressionCodec) }
});
Here is a unit test to demonstrate use of the High Compression Codec:
public class TestCompression {
[Fact]
public void HighCompression() {
FxTest.Setup();
Directory indexDir = new RAMDirectory();
Analyzer standardAnalyzer = new StandardAnalyzer(LuceneVersion.LUCENE_48);
IndexWriterConfig indexConfig = new IndexWriterConfig(LuceneVersion.LUCENE_48, standardAnalyzer);
indexConfig.Codec = new Lucene46HighCompressionCodec(); //<--------Install the High Compression codec.
indexConfig.UseCompoundFile = true;
IndexWriter writer = new IndexWriter(indexDir, indexConfig);
//souce: https://github.com/apache/lucenenet/blob/Lucene.Net_4_8_0_beta00006/src/Lucene.Net/Search/SearcherFactory.cs
SearcherManager searcherManager = new SearcherManager(writer, applyAllDeletes: true, new SearchWarmer());
Document doc = new Document();
doc.Add(new StringField("examplePrimaryKey", "001", Field.Store.YES));
doc.Add(new TextField("exampleField", "Unique gifts are great gifts.", Field.Store.YES));
writer.AddDocument(doc);
doc = new Document();
doc.Add(new StringField("examplePrimaryKey", "002", Field.Store.YES));
doc.Add(new TextField("exampleField", "Everyone is gifted.", Field.Store.YES));
writer.AddDocument(doc);
doc = new Document();
doc.Add(new StringField("examplePrimaryKey", "003", Field.Store.YES));
doc.Add(new TextField("exampleField", "Gifts are meant to be shared.", Field.Store.YES));
writer.AddDocument(doc);
writer.Commit();
searcherManager.MaybeRefreshBlocking();
IndexSearcher indexSearcher = searcherManager.Acquire();
try {
QueryParser parser = new QueryParser(LuceneVersion.LUCENE_48, "exampleField", standardAnalyzer);
Query query = parser.Parse("everyone");
TopDocs topDocs = indexSearcher.Search(query, int.MaxValue);
int numMatchingDocs = topDocs.ScoreDocs.Length;
Assert.Equal(1, numMatchingDocs);
Document docRead = indexSearcher.Doc(topDocs.ScoreDocs[0].Doc);
string primaryKey = docRead.Get("examplePrimaryKey");
Assert.Equal("002", primaryKey);
} finally {
searcherManager.Release(indexSearcher);
}
}
}
While my initial response was via a Lucene.Net github issue, I'm providing the answer here where it will have better visibility to the Lucene.Net community in hopes that it helps others as well. For those interested, there is more background information about using a custom codec towards the end of that issue's thread.

Related

Hazelcast 3.6.1 "There is no suitable de-serializer for type" exception

I am using Hazelcast 3.6.1 to read from a Map. The object class stored in the map is called Schedule.
I have configured a custom serializer on the client side like this.
ClientConfig config = new ClientConfig();
SerializationConfig sc = config.getSerializationConfig();
sc.addSerializerConfig(add(new ScheduleSerializer(), Schedule.class));
...
private SerializerConfig add(Serializer serializer, Class<? extends Serializable> clazz) {
SerializerConfig sc = new SerializerConfig();
sc.setImplementation(serializer).setTypeClass(clazz);
return sc;
}
The map is created like this
private final IMap<String, Schedule> map = client.getMap("schedule");
If I get from the map using schedule id as key, the map returns the correct value e.g.
return map.get("zx81");
If I try to use an SQL predicate e.g.
return new ArrayList<>(map.values(new SqlPredicate("statusActive")));
then I get the following error
Exception in thread "main" com.hazelcast.nio.serialization.HazelcastSerializationException: There is no suitable de-serializer for type 2. This exception is likely to be caused by differences in the serialization configuration between members or between clients and members.
The custom serializer is using Kryo to serialize (based on this blog http://blog.hazelcast.com/comparing-serialization-methods/)
public class ScheduleSerializer extends CommonSerializer<Schedule> {
#Override
public int getTypeId() {
return 2;
}
#Override
protected Class<Schedule> getClassToSerialize() {
return Schedule.class;
}
}
The CommonSerializer is defined as
public abstract class CommonSerializer<T> implements StreamSerializer<T> {
protected abstract Class<T> getClassToSerialize();
#Override
public void write(ObjectDataOutput objectDataOutput, T object) {
Output output = new Output((OutputStream) objectDataOutput);
Kryo kryo = KryoInstances.get();
kryo.writeObject(output, object);
output.flush(); // do not close!
KryoInstances.release(kryo);
}
#Override
public T read(ObjectDataInput objectDataInput) {
Input input = new Input((InputStream) objectDataInput);
Kryo kryo = KryoInstances.get();
T result = kryo.readObject(input, getClassToSerialize());
input.close();
KryoInstances.release(kryo);
return result;
}
#Override
public void destroy() {
// empty
}
}
Do I need to do any configuration on the server side? I thought that the client config would be enough.
I am using Hazelcast client 3.6.1 and have one node/member running.
Queries require the nodes to know about the classes as the bytestream has to be deserialized to access the attributes and query them. This means that when you want to query on objects you have to deploy the model classes (and serializers) on the server side as well.
Whereas when you use key-based access we do not need to look into the values (neither into the keys as we compare the byte-arrays of the key) and just send the result. That way neither model classes nor serializers have to be available on the Hazelcast nodes.
I hope that makes sense.

log4net smtpappender custom email recipients

I am able to use log4net to send logging information to an email address using the smtpappender and a Gmail account in a VB solution (Visual Studio 2010). The recipient is configured in the log4net config file, however I would like to be able to change the recipient email address dynamically.
Is it possible without having to write a custom smtpappender?
Wether the answer is yes or no, please give me an example, preferably in VB.
It's not possible, the current SmtpAppender won't allow it. But you're lucky, the SendBuffer in the SmtpAppender can be overridden, so you can easily add some behavior to it. I think your best bet is to use the LoggingEvent properties to set the recipient:
public class MySmtpAppender : SmtpAppender
{
protected override void SendBuffer(log4net.Core.LoggingEvent[] events)
{
var Recipients = events
.Where(e => e.Properties.Contains("recipient"))
.Select(e => e.Properties["recipient"])
.Distinct();
var RecipientsAsASingleLine = string.Join(";", Recipients.ToArray()); // or whatever the separator is
var PreviousTo = To;
To = RecipientsAsASingleLine;
base.SendBuffer(events);
To = PreviousTo;
}
}
You may want to change the way to select recipients, your call.
edit The tool recommended by stuartd works quite well (well, it is quite a simple class, but still):
Public Class MySmtpAppender
Inherits SmtpAppender
Protected Overrides Sub SendBuffer(events As log4net.Core.LoggingEvent())
Dim Recipients = events.Where(Function(e) e.Properties.Contains("recipient")).[Select](Function(e) e.Properties("recipient")).Distinct()
Dim RecipientsAsASingleLine = String.Join(";", Recipients.ToArray())
' or whatever the separator is
Dim PreviousTo = [To]
[To] = RecipientsAsASingleLine
MyBase.SendBuffer(events)
[To] = PreviousTo
End Sub
End Class
it is possible. see my answer in this question - copied code below
using System;
using System.IO;
using System.Web.Mail;
using log4net.Layout;
using log4net.Core;
using log4net.Appender;
namespace SampleAppendersApp.Appender
{
/// <summary>
/// Simple mail appender that sends individual messages
/// </summary>
/// <remarks>
/// This SimpleSmtpAppender sends each LoggingEvent received as a
/// separate mail message.
/// The mail subject line can be specified using a pattern layout.
/// </remarks>
public class SimpleSmtpAppender : AppenderSkeleton
{
public SimpleSmtpAppender()
{
}
public string To
{
get { return m_to; }
set { m_to = value; }
}
public string From
{
get { return m_from; }
set { m_from = value; }
}
public PatternLayout Subject
{
get { return m_subjectLayout; }
set { m_subjectLayout = value; }
}
public string SmtpHost
{
get { return m_smtpHost; }
set { m_smtpHost = value; }
}
#region Override implementation of AppenderSkeleton
override protected void Append(LoggingEvent loggingEvent)
{
try
{
StringWriter writer = new StringWriter(System.Globalization.CultureInfo.InvariantCulture);
string t = Layout.Header;
if (t != null)
{
writer.Write(t);
}
// Render the event and append the text to the buffer
RenderLoggingEvent(writer, loggingEvent);
t = Layout.Footer;
if (t != null)
{
writer.Write(t);
}
MailMessage mailMessage = new MailMessage();
mailMessage.Body = writer.ToString();
mailMessage.From = m_from;
mailMessage.To = m_to;
if (m_subjectLayout == null)
{
mailMessage.Subject = "Missing Subject Layout";
}
else
{
StringWriter subjectWriter = new StringWriter(System.Globalization.CultureInfo.InvariantCulture);
m_subjectLayout.Format(subjectWriter, loggingEvent);
mailMessage.Subject = subjectWriter.ToString();
}
if (m_smtpHost != null && m_smtpHost.Length > 0)
{
SmtpMail.SmtpServer = m_smtpHost;
}
SmtpMail.Send(mailMessage);
}
catch(Exception e)
{
ErrorHandler.Error("Error occurred while sending e-mail notification.", e);
}
}
override protected bool RequiresLayout
{
get { return true; }
}
#endregion // Override implementation of AppenderSkeleton
private string m_to;
private string m_from;
private PatternLayout m_subjectLayout;
private string m_smtpHost;
}
}
You can use log4Net.GlobalContext class.
code:
App.config
<appender name="SmtpLogAppender" type="log4net.Appender.SmtpAppender">
<to type="log4net.Util.PatternString" value="%property{SenderList}"/>
C# Code
GlobalContext.Properties["SenderList"] = "abc#xyz.com, def#xyz.com";
log4net.Config.XmlConfigurator.Configure();
It is possible, to a certain degree, to get dynamic recipient.
In the SMTP appender the replacements for To, CC, From etc is done during configuration. Not ideal (would be best if it were to calculate the values at every send) but still workable.
Re-configuring the logging is not free but is doable programatically. You can set your To field as such :
<to type="log4net.Util.PatternString" value="SomeAccountThatReceivesAll#yourCorp.com%property{MailRecipient}" />
then in your code you can set a comma separated list of recipient like this :
log4net.GlobalContext.Properties["MailRecipient"] = "SomeOtherAccount#yourCorp.com,YourCorpSupportForThisApp#yourCorp.com";
the important bit is that you force a re-configuration AFTER you set these values. The exact syntax will depend on your config strategy, we use a central config file for all the logging so in C# it would look like this :
log4net.Config.XmlConfigurator.ConfigureAndWatch("PathToYourCentralFile.xml");
and voila ! Dynamic recipient without any custom appenders !
Personally I would prefer the custom appender, less hackish as it does not require constant re-configuring if you need to change them often. But if you only need the 10 minute fix and the config for recipient does not change once started then I found this to be good enough.

How to initialize RavenDb database with bundle programatically?

I have this code it works well EXCEPT I want to ensure that the expiration bundle is in the database and I dont want to create this database manually. What is the correct way to initialize a database with the expiration bundle enabled?
_documentStore = new DocumentStore()
{
Url = SettingsManager.RavenDbUrl,
DefaultDatabase = SettingsManager.RavenDbDatabaseName
};
_documentStore.Initialize();
Well I couldnt find docs and I couldnt find help so I cobbled this together:
static class RavenDbExtensions
{
/// <summary>
/// Ensure a bundle is activated
/// </summary>
/// <param name="documentStore"></param>
/// <param name="bundleName"></param>
/// <param name="databaseName"></param>
public static void ActivateBundle(this IDocumentStore documentStore, string bundleName, string databaseName)
{
using (var session = documentStore.OpenSession())
{
var databaseDocument = session.Load<DatabaseDocument>("Raven/Databases/" + databaseName);
var settings = databaseDocument.Settings;
var activeBundles = settings.ContainsKey(Constants.ActiveBundles) ? settings[Constants.ActiveBundles] : null;
if (string.IsNullOrEmpty(activeBundles))
settings[Constants.ActiveBundles] = bundleName;
else if (!activeBundles.Split(new char[]{';'}).Contains(bundleName, StringComparer.OrdinalIgnoreCase))
settings[Constants.ActiveBundles] = activeBundles + ";" + bundleName;
session.SaveChanges();
}
}
}
Then I do this little dance when initializing the document store. It seems to work well. It isnt clear whether the bundle is named Expiration or DocumentExpiration so I try both it doesnt crash and it seems to fold in the expiration functionality I need.
_documentStore = new DocumentStore()
{
Url = SettingsManager.RavenDbUrl
};
_documentStore.Initialize();
_documentStore.ActivateBundle("Expiration", Assembly.GetExecutingAssembly().GetName().Name);
_documentStore.ActivateBundle("DocumentExpiration", Assembly.GetExecutingAssembly().GetName().Name);
_documentStore.DefaultDatabase = Assembly.GetExecutingAssembly().GetName().Name;

How to get access to the VSTO Application object of multiple instances of Visio 2010 using C#?

Does anyone know how to get access to the VSTO Application object of multiple instances of Visio 2010 using C#? Marshal.GetActiveObject() only returns the active instance.
There are forum posts on how to get multiple Application objects for all instances of Excel - where the author is using the technique to iterate through each Excel instance by process, get the native object model from a child window of the Excel instance, and finally to get the Application object.
This technique works well for for Excel, but I am unable to get a valid native OM (IDispatch or IAccessible) from the call to AccessibleObjectFromWindow() for a Visio window handle that would allow me to reference the parent Application object.
Here's my code snippet:
using VISIO = Microsoft.Office.Interop.Visio;
Process[] processes = Process.GetProcessesByName(Visio.APP_PROCESS_NAME);
foreach (Process process in processes)
{
int hwnd = (int)process.MainWindowHandle;
// We need to enumerate the child windows to find one that
// supports accessibility. To do this, instantiate the
// delegate and wrap the callback method in it, then call
// EnumChildWindows, passing the delegate as the 2nd arg.
if (hwnd != 0)
{
int hwndChild = 0;
cb = new EnumChildCallback(EnumChildProc);
EnumChildWindows(hwnd, cb, ref hwndChild);
// If we found an accessible child window, call
// AccessibleObjectFromWindow, passing the constant
// OBJID_NATIVEOM (defined in winuser.h) and
// IID_IDispatch - we want an IDispatch pointer
// into the native object model.
if (hwndChild != 0)
{
const uint OBJID_NATIVEOM = 0xFFFFFFF0;
Guid IID_IDispatch = new Guid("{00020400-0000-0000-C000-000000000046}");
EXL.Window ptr = null;
int hr = AccessibleObjectFromWindow(
hwndChild, OBJID_NATIVEOM,
IID_IDispatch.ToByteArray(), ref ptr);
if (hr >= 0)
{
// If we successfully got a native OM
// IDispatch pointer, we can QI this for
// an Viso Application (using the implicit
// cast operator supplied in the PIA).
VISIO.Application app = (VISIO.Application)ptr.Application;
allInstances.Add(app);
}
}
}
}
I found a solution that iterates through the Runtime Object Table (ROT) to get all monikers corresponding to Visio documents (".vdx") and get their corresponding VSO.Document object (using VSO=Microsoft.Office.Interop.Visio); with the VSO.Document object, I can get the VSO.Application object from the Document.Application property.
From this article by Andrew Baker: http://www.vbusers.com/codecsharp/codeget.asp?ThreadID=69&PostID=1, I created a ROTUtil utility/helper class:
/// <summary>
/// The COM running object table utility class.
/// </summary>
public class ROTUtil
{
#region APIs
[DllImport("ole32.dll")]
private static extern int GetRunningObjectTable(int reserved,
out UCOMIRunningObjectTable prot);
[DllImport("ole32.dll")]
private static extern int CreateBindCtx(int reserved,
out UCOMIBindCtx ppbc);
[DllImport("ole32.dll", PreserveSig = false)]
private static extern void CLSIDFromProgIDEx([MarshalAs(UnmanagedType.LPWStr)] string progId, out Guid clsid);
[DllImport("ole32.dll", PreserveSig = false)]
private static extern void CLSIDFromProgID([MarshalAs(UnmanagedType.LPWStr)] string progId, out Guid clsid);
[DllImport("ole32.dll")]
private static extern int ProgIDFromCLSID([In()]ref Guid clsid, [MarshalAs(UnmanagedType.LPWStr)]out string lplpszProgID);
#endregion
#region Public Methods
/// <summary>
/// Converts a COM class ID into a prog id.
/// </summary>
/// <param name="progID">The prog id to convert to a class id.</param>
/// <returns>Returns the matching class id or the prog id if it wasn't found.</returns>
public static string ConvertProgIdToClassId(string progID)
{
Guid testGuid;
try
{
CLSIDFromProgIDEx(progID, out testGuid);
}
catch
{
try
{
CLSIDFromProgID(progID, out testGuid);
}
catch
{
return progID;
}
}
return testGuid.ToString().ToUpper();
}
/// <summary>
/// Converts a COM class ID into a prog id.
/// </summary>
/// <param name="classID">The class id to convert to a prog id.</param>
/// <returns>Returns the matching class id or null if it wasn't found.</returns>
public static string ConvertClassIdToProgId(string classID)
{
Guid testGuid = new Guid(classID.Replace("!", ""));
string progId = null;
try
{
ProgIDFromCLSID(ref testGuid, out progId);
}
catch (Exception)
{
return null;
}
return progId;
}
/// <summary>
/// Get a snapshot of the running object table (ROT).
/// </summary>
/// <returns>A hashtable mapping the name of the object in the ROT to the corresponding object
/// <param name="filter">The filter to apply to the list (nullable).</param>
/// <returns>A hashtable of the matching entries in the ROT</returns>
public static Hashtable GetActiveObjectList(string filter)
{
Hashtable result = new Hashtable();
int numFetched;
UCOMIRunningObjectTable runningObjectTable;
UCOMIEnumMoniker monikerEnumerator;
UCOMIMoniker[] monikers = new UCOMIMoniker[1];
GetRunningObjectTable(0, out runningObjectTable);
runningObjectTable.EnumRunning(out monikerEnumerator);
monikerEnumerator.Reset();
while (monikerEnumerator.Next(1, monikers, out numFetched) == 0)
{
UCOMIBindCtx ctx;
CreateBindCtx(0, out ctx);
string runningObjectName;
monikers[0].GetDisplayName(ctx, null, out runningObjectName);
if (filter == null || filter.Length == 0 || runningObjectName.IndexOf(filter) != -1)
{
object runningObjectVal;
runningObjectTable.GetObject(monikers[0], out runningObjectVal);
result[runningObjectName] = runningObjectVal;
}
}
return result;
}
/// <summary>
/// Returns an object from the ROT, given a prog Id.
/// </summary>
/// <param name="progId">The prog id of the object to return.</param>
/// <returns>The requested object, or null if the object is not found.</returns>
public static object GetActiveObject(string progId)
{
// Convert the prog id into a class id
string classId = ConvertProgIdToClassId(progId);
UCOMIRunningObjectTable prot = null;
UCOMIEnumMoniker pMonkEnum = null;
try
{
int Fetched = 0;
// Open the running objects table.
GetRunningObjectTable(0, out prot);
prot.EnumRunning(out pMonkEnum);
pMonkEnum.Reset();
UCOMIMoniker[] pmon = new UCOMIMoniker[1];
// Iterate through the results
while (pMonkEnum.Next(1, pmon, out Fetched) == 0)
{
UCOMIBindCtx pCtx;
CreateBindCtx(0, out pCtx);
string displayName;
pmon[0].GetDisplayName(pCtx, null, out displayName);
Marshal.ReleaseComObject(pCtx);
if (displayName.IndexOf(classId) != -1)
{
// Return the matching object
object objReturnObject;
prot.GetObject(pmon[0], out objReturnObject);
return objReturnObject;
}
}
return null;
}
finally
{
// Free resources
if (prot != null)
Marshal.ReleaseComObject(prot);
if (pMonkEnum != null)
Marshal.ReleaseComObject(pMonkEnum);
}
}
Then here's the method in my class to get the instances:
/// <summary>
/// This strategy of getting the VSO.Application instances uses the appropriate file monikers
/// from the Runtime Object Table to get the VSO.Document object and hence the VSO.Document.Application
/// property.
/// </summary>
/// <param name="allInstances"></param>
protected void GetInstancesROTStrategy(IList<VSO.Application> allInstances)
{
// Iterate through all the objects in the ROT
string filter = ".vdx";
Hashtable runningObjects = ROTUtil.GetActiveObjectList(filter);
Hashtable appInstances = new Hashtable();
// Display the object ids
foreach (DictionaryEntry de in runningObjects)
{
// get visio document file monikers
string progId = de.Key.ToString();
object getObj = ROTUtil.GetActiveObject(progId);
if (getObj != null)
{
// try to cast the object to a VSO.Document
VSO.Document doc = getObj as VSO.Document;
if (doc != null)
{
VSO.Application app = doc.Application;
// only add to results IList if not duplicate
if (!appInstances.ContainsKey(app.WindowHandle32))
{
appInstances.Add(app.WindowHandle32, app);
allInstances.Add(app);
}
}
}
}
}

C# Compact-Framework friendly command line parser

I read this question: Command Line Parser for .NET.
I thought that was what I was looking for, but the library Command Line Parser Library is not Compact framework friendly...
I REALLY don't want to write a CL parser and I have been drifting away from the real purpose of my little app because of this unfortunate trial.
Does someone know of a library that fits the compact-framework? (preferably with simplicity and functionality like the one mentioned above)
Does not matter whether version 2 or 3.5
I developed this framework, maybe it helps:
The SysCommand is a powerful cross-platform framework, to develop Console Applications in .NET. Is simple, type-safe, and with great influences of the MVC pattern.
https://github.com/juniorgasparotto/SysCommand
namespace Example.Initialization.Simple
{
using SysCommand.ConsoleApp;
public class Program
{
public static int Main(string[] args)
{
return App.RunApplication();
}
}
// Classes inheriting from `Command` will be automatically found by the system
// and its public properties and methods will be available for use.
public class MyCommand : Command
{
public void Main(string arg1, int? arg2 = null)
{
if (arg1 != null)
this.App.Console.Write(string.Format("Main arg1='{0}'", arg1));
if (arg2 != null)
this.App.Console.Write(string.Format("Main arg2='{0}'", arg2));
}
public void MyAction(bool a)
{
this.App.Console.Write(string.Format("MyAction a='{0}'", a));
}
}
}
Tests:
// auto-generate help
$ my-app.exe help
// method "Main" typed
$ my-app.exe --arg1 value --arg2 1000
// or without "--arg2"
$ my-app.exe --arg1 value
// actions support
$ my-app.exe my-action -a
This is what I'm using. I borrowed it from somewhere, but not sure where:
using System.Collections.Specialized;
using System.Text.RegularExpressions;
/// <summary>
/// Parses the command line arguments into a name/value collection
/// </summary>
public class CommandLineArgumentParser
{
#region Fields
private StringDictionary parameters;
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="CommandLineArgumentParser"/> class.
/// </summary>
/// <param name="args">command-line arguments
/// </param>
public CommandLineArgumentParser(string[] args)
{
this.parameters = new StringDictionary();
Regex spliter = new Regex(#"^-{1,2}|^/|=|:", RegexOptions.IgnoreCase | RegexOptions.Compiled);
Regex remover = new Regex(#"^['""]?(.*?)['""]?$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
string parameter = null;
string[] parts;
// Valid parameters forms:
// {-,/,--}param{ ,=,:}((",')value(",'))
// Examples:
// -param1 value1 --param2 /param3:"Test-:-work"
// /param4=happy -param5 '--=nice=--'
foreach (string txt in args)
{
// Look for new parameters (-,/ or --) and a
// possible enclosed value (=,:)
parts = spliter.Split(txt, 3);
switch (parts.Length)
{
// Found a value (for the last parameter
// found (space separator))
case 1:
if (parameter != null)
{
if (!this.parameters.ContainsKey(parameter))
{
parts[0] = remover.Replace(parts[0], "$1");
this.parameters.Add(parameter, parts[0]);
}
parameter = null;
}
// else Error: no parameter waiting for a value (skipped)
break;
// Found just a parameter
case 2:
// The last parameter is still waiting.
// With no value, set it to true.
if (parameter != null)
{
if (!this.parameters.ContainsKey(parameter))
{
this.parameters.Add(parameter, "true");
}
}
parameter = parts[1];
break;
// Parameter with enclosed value
case 3:
// The last parameter is still waiting.
// With no value, set it to true.
if (parameter != null)
{
if (!this.parameters.ContainsKey(parameter))
{
this.parameters.Add(parameter, "true");
}
}
parameter = parts[1];
// Remove possible enclosing characters (",')
if (!this.parameters.ContainsKey(parameter))
{
parts[2] = remover.Replace(parts[2], "$1");
this.parameters.Add(parameter, parts[2]);
}
parameter = null;
break;
}
}
// In case a parameter is still waiting
if (parameter != null)
{
if (!this.parameters.ContainsKey(parameter))
{
this.parameters.Add(parameter, "true");
}
}
}
#endregion
#region Properties
/// <summary>
/// Gets a count of command line arguments
/// </summary>
public int Count
{
get
{
return this.parameters.Count;
}
}
/// <summary>
/// Gets the value with the given parameter name
/// </summary>
/// <param name="param">name of the parameter</param>
/// <returns>the value of the parameter</returns>
public string this[string param]
{
get
{
return this.parameters[param];
}
}
#endregion
}
http://commandline.codeplex.com/ I've used this so many times I've lost count. Maybe it works for CE. If not, it'll provide a fantastic starting point.