Andrew Truckle Andrew Truckle - 2 months ago 10
C# Question

Passing back an array of custom objects from C#.NET DLL to MFC

Here is an example XML data file:

<?xml version="1.0" encoding="utf-8"?>
<AssignmentHistory Version="171804">
<W20160104>
<StudentItems>
<Item>
<Name Counsel="13" NextCounsel="0" Completed="1">Name 1</Name>
<Type>Bible Reading (Main)</Type>
</Item>
</StudentItems>
</W20160104>
<W20160111>
<StudentItems>
<Item>
<Name Counsel="9" NextCounsel="9" Completed="0">Name 2</Name>
<Type>Bible Reading (Main)</Type>
</Item>
<Item Description="Initial Call">
<Name Counsel="37" NextCounsel="38" Completed="1">Name 1</Name>
<Type>#1 Student (Main)</Type>
</Item>
<Item>
<Name>Name 3</Name>
<Type>Assistant</Type>
</Item>
<Item>
<Name Counsel="48" NextCounsel="49" Completed="1">Name 4</Name>
<Type>#2 Student (Main)</Type>
</Item>
<Item>
<Name>Name 5</Name>
<Type>Assistant</Type>
</Item>
<Item>
<Name Counsel="27" NextCounsel="30" Completed="1">Name 6</Name>
<Type>#3 Student (Main)</Type>
</Item>
<Item>
<Name>Name 7</Name>
<Type>Assistant</Type>
</Item>
</StudentItems>
</W20160111>
</AssignmentHistory>


I have written some code that reads the XML data file and locates future assignment history for a given name. For example, if the week is 4 Jan 2016, and we are getting the history for Name 1, then my code will return a list of entries (in this case, just 1, for the week on 11 Jan 2016).

My code:

My method is behaving fine:

public void ExtractFutureStudentHistory(String strStudent, DateTime datWeekOfMeeting, String strHistoryDatabase, out DateTime[] aryFutureDates, out string[] aryFutureAssignTypes, out int[] aryFutureStudyNo)
{
XmlDocument docAssignHistory = new XmlDocument();

aryFutureDates = null;
aryFutureAssignTypes = null;
aryFutureStudyNo = null;

List<DateTime> listFutureDates = new List<DateTime>();
List<string> listFutureAssignTypes = new List<string>();
List<int> listFutureStudyNo = new List<int>();

try
{
docAssignHistory.Load(strHistoryDatabase);

// The data in the XML should already be in ascending date order

// The data we want:

// AssignmentHistory/<WYYYYMMDD>/StudentItems/Item/Name
// AssignmentHistory/<WYYYYMMDD>/StudentItems/Item/Type
XmlNodeList listHistory = docAssignHistory.SelectNodes("AssignmentHistory/*/StudentItems/Item[Name='" + strStudent + "']");
foreach(XmlNode nodeHistoryItem in listHistory)
{
XmlNode weekNode = nodeHistoryItem.ParentNode.ParentNode;
String strWeekDate = weekNode.Name.Substring(1); // This skips the preceding "W"

DateTime dateHistoryItemWeekOfMeeting = new DateTime(Convert.ToInt32(strWeekDate.Substring(0, 4)),
Convert.ToInt32(strWeekDate.Substring(4, 2)),
Convert.ToInt32(strWeekDate.Substring(6, 2)));

if (dateHistoryItemWeekOfMeeting.Date > datWeekOfMeeting.Date)
{
// We need to include it
listFutureDates.Add(dateHistoryItemWeekOfMeeting);
listFutureStudyNo.Add(Convert.ToInt32(nodeHistoryItem.SelectSingleNode("Name").Attributes["Counsel"].Value));
listFutureAssignTypes.Add(nodeHistoryItem.SelectSingleNode("Type").InnerText);
}
}

aryFutureDates = listFutureDates.ToArray();
aryFutureStudyNo = listFutureStudyNo.ToArray();
aryFutureAssignTypes = listFutureAssignTypes.ToArray();
}
catch (Exception ex)
{
SimpleLog.Log(ex);
}
}


Possibly the logic can be simplified, but it works. My issue comes with the fact that my method is part of a C# .NET DLL. At the moment then I have this public interface method:

[Guid("xx")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[ComVisible(true)]
public interface IMSAToolsLibraryInterface
{
void ExtractFutureStudentHistory(String strStudent, DateTime datWeekOfMeeting, String strHistoryDatabase, out DateTime[] aryFutureDates, out string[] aryFutureAssignTypes, out Int32[] aryFutureStudyNo);

}


It works fine. In C++ MFC I have three
SAFEARRAY**
objects, one of type
BSTR
, one of type
DATE
and one of type
int
. No problem with that it itself.

My question is, can my function be changed to output a single list of objects? For example, if I created a class:

StudentItem
that had three member variables for the date, assignment type and study number.

I tried changing my function parameter to
out List<StudentItem>
but that didn't work. Then I changed it to
out StudentItem[]
and I still couldn't work with it.

I declared
StudentItem
as a basic
struct
with three members. What is the right way to declare this object so I can pass it back as an array for working with in MFC?

Thanks.

Update



Step 1:

I added a new object to the DLL project:

[Guid("xx")]
[ComVisible(true)]
public struct StudentItem
{
public string Type { get; set; }
public DateTime Week { get; set; }
public int Study { get; set; }
}


Step 2:

I add a reference in the interface:

void ExtractFutureStudentHistory2(String strStudent, DateTime datWeekOfMeeting, String strHistoryDatabase, out StudentItem[] aryStudentItems);


Step 3:

I add the adjusted method:

public void ExtractFutureStudentHistory2(String strStudent, DateTime datWeekOfMeeting, String strHistoryDatabase, out StudentItem[] aryStudentItems)
{
XmlDocument docAssignHistory = new XmlDocument();

aryStudentItems = null;

List<StudentItem> listStudentItems = new List<StudentItem>();

try
{
docAssignHistory.Load(strHistoryDatabase);

// The data in the XML should already be in ascending date order

// The data we want:

// AssignmentHistory/<WYYYYMMDD>/StudentItems/Item/Name
// AssignmentHistory/<WYYYYMMDD>/StudentItems/Item/Type
XmlNodeList listHistory = docAssignHistory.SelectNodes("AssignmentHistory/*/StudentItems/Item[Name='" + strStudent + "']");
foreach (XmlNode nodeHistoryItem in listHistory)
{
XmlNode weekNode = nodeHistoryItem.ParentNode.ParentNode;
String strWeekDate = weekNode.Name.Substring(1); // This skips the preceding "W"

DateTime dateHistoryItemWeekOfMeeting = new DateTime(Convert.ToInt32(strWeekDate.Substring(0, 4)),
Convert.ToInt32(strWeekDate.Substring(4, 2)),
Convert.ToInt32(strWeekDate.Substring(6, 2)));

if (dateHistoryItemWeekOfMeeting.Date > datWeekOfMeeting.Date)
{
StudentItem oItem = new StudentItem();
oItem.Week = dateHistoryItemWeekOfMeeting;
oItem.Type = nodeHistoryItem.SelectSingleNode("Type").InnerText;
oItem.Study = Convert.ToInt32(nodeHistoryItem.SelectSingleNode("Name").Attributes["Counsel"].Value);

listStudentItems.Add(oItem);
}
}

aryStudentItems = listStudentItems.ToArray();
}
catch (Exception ex)
{
SimpleLog.Log(ex);
}
}


Step 4:

I compile the DLL. I get a problem:


1>C:\Program Files (x86)\Microsoft Visual
Studio\2017\Community\MSBuild\15.0\Bin\Microsoft.Common.CurrentVersion.targets(4556,5):
warning : Type library exporter warning processing
'MSAToolsLibrary.StudentItem.k__BackingField, MSAToolsLibrary'.
Warning: The public struct contains one or more non-public fields that
will be exported.

Answer Source

You have two options here:

Remove the auto-implemented properties. This exposes your struct in a well-known fashion, which can be used by COM clients:

[Guid("xx")]
[ComVisible(true)]
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct StudentItem
{
    [MarshalAs(UnmanagedType.BStr)]
    public string Type;
    public DateTime Week;
    public int Study;
}

... or use an interface. Properties are fully supported in COM interfaces:

[Guid("xx")]
[ComVisible(true)]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface StudentItem
{
    string Type { get; set; }
    DateTime Week { get; set; }
    int Study { get; set; }
}

As a side note, you might consider changing your method as follows:

StudentItem[] ExtractFutureStudentHistory2(String strStudent, DateTime datWeekOfMeeting, String strHistoryDatabase);

This makes it easier to use the method in your client, since the array is now declared as a standard return parameter.