I have a silverlight app using Prism practices; the current code does a search by first name or last name or gender. regaring the names, I would like to alter the code to somethng like 3 characters because now it is searching as long as one character is found the name will display so you can see the issue, can I adjust the code here to only select those with a 3 character match? lets leave alone the issue of a name with less than 3 but we can allow anything there then.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace PBM.Web.Classes
{
public class Search
{
public static IQueryable<Patient> GetSearchQueryPatient(IQueryable<Patient> pSearchQuery, Patient pPatient)
{
if (!string.IsNullOrEmpty(pPatient.FirstName))
{
pSearchQuery = pSearchQuery.Where(item => item.FirstName.Contains(pPatient.FirstName));
}
if (!string.IsNullOrEmpty(pPatient.LastName))
{
pSearchQuery = pSearchQuery.Where(item => item.LastName.Contains(pPatient.LastName));
}
if (pPatient.Gender.HasValue && pPatient.Gender.Value > 0)
{
pSearchQuery = pSearchQuery.Where(item => item.Gender.Value == pPatient.Gender.Value);
}
pSearchQuery = pSearchQuery.OrderBy(item => item.FirstName).ThenBy(item => item.LastName);
return pSearchQuery;
}
}
}
If I've read your requirement and sample code correctly, simply add a length check to your tests should work:
if (!string.IsNullOrEmpty(pPatient.FirstName) && pPatient.FirstName.Length > 2)
{
pSearchQuery = pSearchQuery.Where(item => item.FirstName.Contains(pPatient.FirstName));
}
It does mean that if the name is less than 3 characters it won't match at all, so what you want to do is then check if this search returned anything and if not do the simple any length search:
if (!string.IsNullOrEmpty(pPatient.FirstName))
{
// First look for a 3 or more character match
if (pPatient.FirstName.Length > 2)
{
pSearchQuery = pSearchQuery.Where(item => item.FirstName.Contains(pPatient.FirstName));
}
// If didn't find anything do the simple search
if (!pSearchQuery.Any())
{
pSearchQuery = pSearchQuery.Where(item => item.FirstName.Contains(pPatient.FirstName));
}
}
Related
I'm trying to implement diacritics-insensitive search with MongoDB C# driver, e.g. when I search for "Joao", it should return all the results containing both "João" and "Joao" (if any).
The following command works on MongoDBCompass, i.e. if I run it against my MongoDB collection (currently only containing a document with "João", none with "Joao"), it will return the correct document:
db.profiles.find({FirstName:"Joao"}).collation({locale:"pt", strength: 1})
However, when I try to transpose it to C#, it won't work (e. g. doesn't return any result if I search for "Joao", only if I search for "João"):
private IFindFluent<ProfessionalProfile, ProfessionalProfile> BuildProfessionalLocationFilter(BaseQuery criteria)
{
FilterDefinition<ProfessionalProfile> filter = FilterDefinition<ProfessionalProfile>.Empty;
if (!string.IsNullOrEmpty(criteria.SearchWords))
{
var searchWords = criteria.SearchWords.ToLower().Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList();
FilterDefinition<ProfessionalProfile> searchWordFilter = FilterDefinition<ProfessionalProfile>.Empty;
foreach (string searchWord in searchWords)
{
var newFilter = Builders<ProfessionalProfile>.Filter.And(Builders<ProfessionalProfile>.Filter.Where(profile =>
profile.FirstName.ToLower().Contains(searchWord) ||
profile.LastName.ToLower().Contains(searchWord) ||
profile.Description.ToLower().Contains(searchWord) ||
profile.Email.ToLower().Contains(searchWord) ||
profile.Facebook.ToLower().Contains(searchWord) ||
profile.Instagram.ToLower().Contains(searchWord) ||
profile.LinkedIn.ToLower().Contains(searchWord) ||
profile.Locations.Any(location =>
location.Name.ToLower().Contains(searchWord) ||
location.District.Name.ToLower().Contains(searchWord) ||
location.Council.Name.ToLower().Contains(searchWord)) &&
!profile.IsDeleted &&
profile.WizardStep == 6));
if (searchWordFilter == FilterDefinition<ProfessionalProfile>.Empty)
searchWordFilter = newFilter;
else
searchWordFilter = searchWordFilter | newFilter;
}
filter = filter & searchWordFilter;
}
IFindFluent<ProfessionalProfile, ProfessionalProfile> findFluent = _professionalCollection.Find(filter);
findFluent.Options.Collation = new Collation("pt", strength: CollationStrength.Primary);
//IFindFluent<ProfessionalProfile, ProfessionalProfile> findFluent = _professionalCollection.Find(filter, new FindOptions() { Collation = new Collation("pt", strength: CollationStrength.Primary) } );
return findFluent;
}
Please note that I've also tried the commented line above, with the same (predictable) results.
What could be missing? Thanks.
EDIT:
As asked in the comment below by #Đĵ ΝιΓΞΗΛψΚ, I'm adding some information about the profile collection collation.
Mongo shell command:
db.getCollectionInfos({ name: 'profiles' })
Ouput:
[
{
name: 'profiles',
type: 'collection',
options: {},
info: {
readOnly: false,
uuid: UUID("4606f027-03b1-45e8-bf7f-6c99461db042")
},
idIndex: { v: 2, key: [Object], name: '_id_', ns: 'dev.profiles' }
}
]
it took a bit of investigating since documentation on the issue is scarse, but i think i figured out what's happening.
profile.FirstName.ToLower().Contains(searchWord) gets translated by the driver to a $regex query.
from what i can see, the regex search in mongodb is not collation aware. so you can't use regex functionality to do diacritic insensitive searches.
however, the solution to your requirement is to create a Text Index containing all of the fields you want to search in and utilize that index to do a diacritic & case insensitive search for your search words. it will also be the most efficient way to achieve your requirement.
the one limitation of using a text index is that it won't let you search for partial matches of words such as Jo. mongodb fulltext search only works on complete words unfortunately.
here's a test program (using mongodb.entities library for brevity):
using MongoDB.Driver;
using MongoDB.Entities;
using System.Threading.Tasks;
namespace TestApplication
{
public class ProfessionalProfile : Entity
{
public string FirstName { get; set; }
public string LastName { get; set; }
public bool IsDeleted { get; set; }
public int WizardStep { get; set; }
}
public static class Program
{
private static async Task Main()
{
await DB.InitAsync("test", "localhost");
await DB.Index<ProfessionalProfile>()
.Key(p => p.FirstName, KeyType.Text)
.Key(p => p.LastName, KeyType.Text)
.CreateAsync();
await new[] {
new ProfessionalProfile{ FirstName = "João", LastName = "Balboa", IsDeleted = false, WizardStep = 6},
new ProfessionalProfile{ FirstName = "Joao", LastName = "Balboa", IsDeleted = false, WizardStep = 6},
}.SaveAsync();
IAggregateFluent<ProfessionalProfile> aggregateFluent =
DB.FluentTextSearch<ProfessionalProfile>(
searchType: Search.Full,
searchTerm: "Joao Mary John",
caseSensitive: false,
diacriticSensitive: false)
.Match(p => !p.IsDeleted && p.WizardStep == 6);
var result = await aggregateFluent.ToListAsync();
}
}
}
i'm sure you won't have much trouble translating that to mongo driver code.
I have a list(in txt file) that looks like this field:description
field20D.name = Reference
field20[101].name = Sender's Reference
field20[102].name = File Reference
field20[102_STP].name = File Reference
field20[103].name = Sender's Reference
The numbers in [] like 101,102 are messagetype.
How can i write the code so when i have a property with any value in that list, to get the equivalent description for it.
example: when a field has a value "20D" to build a string "20D - Reference"
Here's a good class that can do what you stated above.
using System;
using System.Collections.Generic;
namespace TestConsoleProject
{
public class WeirdLineFormatReader
{
public IEnumerable<Tuple<string, string>> ReadLines(IEnumerable<string> lines)
{
foreach (string line in lines)
{
// split each line on the =
string[] strLineArray = line.Split('=');
// get the first and second values of the split line
string field = strLineArray[0];
string value = strLineArray[1];
// remove the first field word
field = field.Substring("field".Length);
// remove the .name portion
field = field.Replace(".name", "");
// remove the surrounding white-space
field = field.Trim();
// remove all white space before/after the description
value = value.Trim();
yield return new Tuple<string, string>(field, value);
}
}
}
}
Here's a quick console project that will use the class to output your format to the console the way you want.
using System;
using System.IO;
namespace TestConsoleProject
{
class Program
{
static void Main(string[] args)
{
var lines = File.ReadLines(args[0]);
var reader = new WeirdLineFormatReader();
var tuples = reader.ReadLines(lines);
foreach (var tuple in tuples)
Console.WriteLine("{0} - {1}", tuple.Item1, tuple.Item2);
Console.ReadKey();
}
}
}
Just for the fun of it and also because I suspect you are only showing us a couple of the lines in a much larger text file; here's a format for unit testing when you find that you need to add more code to the ReadLines(string[]) method later.
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Linq;
using TestConsoleProject;
namespace UnitTestProject
{
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestFormatter_WithoutBrackets()
{
// Arrange
var reader = new WeirdLineFormatReader();
string[] lines = {
"field20D.name = Reference"
};
// Act
var tuples = reader.ReadLines(lines).ToList();
// Assert
Assert.AreEqual(tuples[0].Item1, "20D", "Field 20D did not format correctly, Actual:" + tuples[0].Item1);
Assert.AreEqual(tuples[0].Item2, "Reference", "Field 20D's description did not format correctly, Actual:" + tuples[0].Item2);
}
[TestMethod]
public void TestFormatter_WithBrackets()
{
// Arrange
var reader = new WeirdLineFormatReader();
string[] lines = {
"field20[103].name = Sender's Reference"
};
// Act
var tuples = reader.ReadLines(lines).ToList();
// Assert
Assert.AreEqual(tuples[0].Item1, "20[103]", "Field 20[103] did not format correctly, Actual:" + tuples[0].Item1);
Assert.AreEqual(tuples[0].Item2, "Sender's Reference", "Field 20[103]'s description did not format correctly, Actual:" + tuples[0].Item2);
}
}
}
Using this unit test project you can quickly write new tests for edge cases that you discover. After you modify the ReadLines() method, you can re-run all the unit tests to see if you broke any of the older tests.
Pseudo:
create a dictionary of key value pairs
split on carriage return
split on equals sign (can there be an equals sign in the value?)
regex the first half to get everything between 'field' and '.name', put that in the key
put the split from after the equals sign in the value
now you can reference your dictionary by key:
entry = myDictionary["20D"];
return $"{entry.Key} - {entry.value}";
I've got a textfile which contains the following data:
name = Very well sir
age = 23
profile = none
birthday= germany
manufacturer = Me
And I want to get the profile, birthday and manufacturer value but can't seem to get it right. I succeded including the file into my program but there it stops. I just can't figure out how I will clean the textfile up.
Here's my current code: http://sv.paidpaste.com/HrQXbg
using System;
using System.IO;
using System.Linq;
class Program
{
static void Main()
{
var data = File
.ReadAllLines("test.txt")
.Select(x => x.Split('='))
.Where(x => x.Length > 1)
.ToDictionary(x => x[0].Trim(), x => x[1]);
Console.WriteLine("profile: {0}", data["profile"]);
Console.WriteLine("birthday: {0}", data["birthday"]);
Console.WriteLine("manufacturer: {0}", data["manufacturer"]);
}
}
I would suggest instead of using ReadToEnd, reading each line and doing a string.Split('=') and then a string.Trim() on each line text. You should be left with 2 values per line, the first being the key and the second, the value.
For example, in your reading loop:
List<string[]> myList = new List<string[]>();
string[] splits = nextLine.Split('=');
if (splits.Length == 2)
myList.Add(splits);
You need to split into lines first and then split the lines:
StreamReader reader = new StreamReader(filePath);
string line;
while(null != (line=reader.Read())
{
string[] splitLine = strLines.Split('=');
//Code to find specific items based on splitLine[0] - Example
//TODO: Need a check for splitLine length
case(splitLine[0].ToLower().Trim())
{
case "age": { age = int.Parse(splitLine[1]);
}
}
reader.Dispose();
This should make a good start for you.
At least 3 ways I can think of that this could be done:-
1st (ideal) - in a single telerik grid which has around 8 columns, 1st col would list all table entries with the next 6 for displaying different dates submitted for each entry but not all necessarily having a value for each entry, final col would link to each entry on a separate page to allow new dates to be submitted via datepicker or to be edited.
Main problem is I need to be able to display each of the dates on the grid in different colours depending on each col, by this I mean I record a date in 1st col of which has a yearly renewal so if >6months then it's colour 1, >1month colour 2, <1month colour 3 and finally if past 1 year mark then colour 4.
There are also 2 different possible renewal lengths for the other col's.
2nd - Each different renewal length would get its own grid so 1st for 1y, 2nd for 2nd length and 3rd for 3rd length.
3rd (likely) - 4 grids to replace the colours it would simply display each category so 1 grid would show all entries which had more than 6months, grid 2 would show greater than 1month, grid 3 would show less than 1month and grid 4 would show past time length.
I have no clue how best to sort the dates out in a way that would do what I need it to but I figure either option 1 will be possible or option 3 is the simplest.
Edit -
using System
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace (...).Models.DTO
{
public class ...DTO
{
public int Id { get; set; }
public string Name { get; set; }
//public string C1D
//{
// get
// {
// if (C1D < DateTime.Today.AddDays(-183)) return "Green";
// }
//}
public string C1D
{
get
{
if ((C1D = DateTime.ParseExact(C1D, "yyyy/mm/dd", null)) < DateTime.Today.AddDays(-183)) return "Green";
}
set;
}
public string C2D { get; set; }
Here it shows how I have tried to setup C1D in 2 different ways and for C2D how I usually setup the cols which go into the telerik grid.
[GridAction]
public ActionResult _List(int? Id)
{
List<...DTO> ret = new List<...DTO>();
_db.(...).ToList().ForEach(x =>
{
ret.Add(new ...DTO
{
Id = x.Id,
Name = x.(...)Name,
C1D = (x.C1SD.HasValue) ? x.C1SD.Value.ToShortDateString() : "",
C2D = (x.C2SD.HasValue) ? x.C2SD.Value.ToShortDateString() : "",
This is how I would go about setting it up in the controller for displaying the data in the telerik grid.
Below is how I setup the view
<% Html.Telerik().Grid<(...).Models.DTO.(...)DTO>()
.Name("...List")
.DataKeys(dk => dk.Add(x => x.Id))
.Columns(c =>
{
c.Bound(x => x.Name);
c.Bound(x => x.C1D)
.Title("...");
c.Bound(x => x.C2D)
.Title("...");
c.Bound(x => x.C3D)
.Title("...");
c.Bound(x => x.C4D)
.Title("...");
c.Bound(x => x.C5D)
.Title("...");
c.Bound(x => x.C6D)
.Title("...");
c.Bound(x => x.C7D)
.Title("...");
})
.Sortable()
.Filterable()
.DataBinding(db => db.Ajax().Select("_List", "..."))
.Render();
%>
Edit 2 -
I've also tried
.ClientEvents(e => e.OnDataBound("onDataBound"))
function onDataBound(e) {
if (e.dataItem.C1D > DateTime.Today.AddDays(183)) {
e.cell.style.backgroundColor = "green";
}
if (e.dataItem.C1D > DateTime.Today.AddDays(30)) {
e.cell.style.backgroundColor = "orange";
}
if (e.dataItem.C1D > DateTime.Today) {
e.cell.style.backgroundColor = "red";
}
if (e.dataItem.C1D <= DateTime.Today) {
e.cell.style.backgroundColor = "purple";
}
}
and upon reaching this page it would break into code and say "Microsoft JScript runtime error: 'dataItem.C1D' is null or not an object" and "Microsoft JScript runtime error: 'cell.style' is null or not an object" and then display the page with all the dates in the grid so those items aren't null but is there otherwise some other code/format I should be using to perform this function?
And also looked at http://demos.telerik.com/aspnet-mvc/grid/customformatting in regards to .cellaction like below
.CellAction(cell =>
{
if (cell.Column.Title == "Title Name")
{
if (cell.DataItem.C1D > DateTime.Today.AddDays(183))
{
//Set the background of this cell only
cell.HtmlAttributes["style"] = "background:red;";
}
}
})
and I had to change .Name to .Title since it didn't recognise .Name, but I get the error msg "Error 1 Operator '>' cannot be applied to operands of type 'string' and 'System.DateTime' " so seems I won't be able to perform this complex a task in a cell action.
I've also posted this on the telerik forums attached to another question but so far no reply
http://www.telerik.com/community/forums/aspnet-mvc/grid/telerik-grid-row-custom-formatting-on-either-bit-int-string-field.aspx
Edit 3 -
Additional Controller code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using (Database Name).Models;
using (Database Name).Models.DTO;
using Telerik.Web.Mvc;
using Telerik.Web.Mvc.UI;
namespace (Database Name).Controllers
{
public class (Controller Name)Controller : Controller
{
(Database Name)Entities _db = new (Database Name)Entities();
public ActionResult List()
{
return View();
}
That's it now there's nothing left which I can possibly provide since there's nothing else which could have any kind of affect on the telerik grid so if there is still something else which might be hidden some place else that I may be missing then please explain what that might be since the only thing I haven't included is the code to do with the Create and Edit pages but all they involve is making each simple record then allowing the user to change the dates recorded.
Edit 3 :
When you do :
When you do _db.(...).ToList().ForEach(x =>
{
ret.Add(new ...DTO
{
Id = x.Id,
Name = x.(...)Name,
C1D = (x.C1SD.HasValue) ? x.C1SD.Value.ToShortDateString() : "",
C2D = (x.C2SD.HasValue) ? x.C2SD.Value.ToShortDateString() : "",
}
}
x would be the ObjectFromDB, you don't want to assign each properties of the DTO, you want to pass the baseObject (witch is x), then return the value you want from x.
If you can provide me with youre solution using putfile or something else I can take a look at it if you want to but right now I don't know how it would be possible to help you more than that...
End Edit 3
Can you put some code?
I'll go with solution 1.
You could add a css class to a using ClientTemplate, if it's > [timespan], I think that you should add a colum that is bound on a property that could return name of a css class or an empty string depending on the time span. Let say you have a DateCol1 property witch is a DateTime you could add a DateCol1Css property that goes like this :
public string DateCol1Css
{
get
{
if(DateCol1 < DateTime.Now.AddMonths(-1)) return "Color1"; //witch is less than a month
if(DateCol1 < DateTime.Now.AddMonths(-3)) return "Color2"; //witch is less than 3 months
if(DateCol1 < DateTime.Now.AddMonths(-6)) return "Color3"; //witch is less than 6 months
return "";
}
}
public string DateCol2Css
{
get
{
if (DateCol2 < DateTime.Now.AddDays(-10)) return "Color1"; //witch is less than 10 days
if (DateCol2 < DateTime.Now.AddDays(-30)) return "Color2"; //witch is less than 30 days
return "";
}
}
public string DateCol3Css
{
get
{
if (DateCol3 < DateTime.Now.AddMonths(-1)) return "Color1"; //witch is less than a month
if (DateCol3 < DateTime.Now.AddMonths(-3)) return "Color2"; //witch is less than 3 months
if (DateCol3 < DateTime.Now.AddMonths(-6)) return "Color3"; //witch is less than 6 months
return "";
}
}
And the grid should be like this :
<%= Html.Telerik().Grid<SerializableAdmin>()
.Name("Grid")
.Columns(colums =>
{
colums.Bound(c => c.FirstName);
colums.Bound(c => c.Id);
colums.Bound(c => c.Id).ClientTemplate("<span class=\"<#=DateCol1Css#>\"<#=DateCol1#></span>");
colums.Bound(c => c.Id).ClientTemplate("<span class=\"<#=DateCol2Css#>\"<#=DateCol2#></span>");
colums.Bound(c => c.Id).ClientTemplate("<span class=\"<#=DateCol3Css#>\"<#=DateCol3#></span>");
})
%>
Edit :
Take a look at this code, you pass the object from the database to your new object and add property with get only on the db object.
public class ObjectDTO
{
public ObjectFromDB BaseObject { get; set; }
public int Id
{
get { return BaseObject.Id; }
}
public string Name
{
get { return BaseObject.Name; }
}
public string C1D
{
get
{
if (BaseObject.C1SC.HasValue && BaseObject.C1SC < DateTime.Now.AddDays(-183)) return "Green";
return string.Empty;
}
}
public string C2D
{
get
{
if (BaseObject.C2SC.HasValue && BaseObject.C2SC < DateTime.Now.AddDays(-183)) return "Green";
return string.Empty;
}
}
}
[GridAction]
public ActionResult _List(int? Id)
{
List<ObjectDTO> ret = new List<ObjectDTO>();
_db.GetObjectFromDB().ToList().ForEach(x =>
{
ret.Add(new ObjectDTO { ObjectFromDB = x } );
});
}
For this code block, have you try to cast the string in datetime?
.CellAction(cell =>
{
if (cell.Column.Title == "Title Name")
{
if (!string.IsNullOrEmpty(cell.DataItem.C1D) && DateTime.ParseExact(cell.DataItem.C1D, "yyyy/mm/dd", null) > DateTime.Today.AddDays(183))
{
//Set the background of this cell only
cell.HtmlAttributes["style"] = "background:red;";
}
}
})
And your ...Dto property for the CssColor should be like this :
public class ...DTO
{
public int Id { get; set; }
public string Name { get; set; }
public string C1D
{
get
{
if (!C1SD.HasValue) return string.Empty;
return (DateTime.ParseExact(C1SD, "yyyy/mm/dd", null) < DateTime.Today.AddDays(-183)) ? "Green" : "";
}
}
}
So your GridAction would be like this :
[GridAction]
public ActionResult _List(int? Id)
{
List<...DTO> ret = _db.(...).ToList();
...
Let me know if it helps!
This was what I tried to do from the start
[GridAction]
public ActionResult _List(int? Id)
{
List<...DTO> ret = new List<...DTO>();
_db.(...).ToList().ForEach(x =>
{
ret.Add(new ...DTO
{
Id = x.Id,
Name = x.(...)Name,
C1D = (x.C1SD.HasValue) ? x.C1SD.Value.ToShortDateString() : "",
C2D = (x.C2SD.HasValue) ? x.C2SD.Value.ToShortDateString() : "",
Trick is to do all the calculations in the controller and leave the model and view very basic.
Model- left em all as string and basically did public string blah { get; set;} for each date col and then for each col you want to do something complex like my date calculations you would make an additional col, this would be for the colour/whatever feature you want heck you could even setup an admin function so if they don't have win auth or aren't in correct role etc then it would splarf the data or de-link a url link.
Controller- well as you can see above thats how I sorted out the dates showing up and now the surprisingly simple way to sort out the colour or w/e, (example thing blahDTO bdt = new blahDTO(); )
if (x.TestVal1 != null)
{
if ((x.TestVal1) > (DateTime.Today.AddMonths(6)))
{
bdt.Colourflag1 = "green";
}
Now it doesn't have to be green, it could be true false tom dick or jane w/e but it would just have to be a value assigned based on certain unique conditions.
View- when I realised it could be this easy I facepalmed myself, anyway yeh so c.Bound(x => x.Colourflag1).Hidden(true); next step
.ClientEvents(events => events.OnRowDataBound("onRowDataBound"))
<script type="text/javascript">
function onRowDataBound(e) {
if (e.dataItem.TestVal1 == "green") {
e.row.cells[1].style.backgroundColor = "green";
}
and hey presto you just turn the 1st row/col cell green and this can be twisted and used into w/e e.row.cell[?]. can be used for and you have a single cell do all magic ye ha rolled into 1.
Now I know my jscript code is wasteful since I'm sure at this point you could make the green be an object which would then affect the the next object so if yellow then it makes the background colour code fit in yellow.
If anybody has any questions or jscript advice feel free to ask/comment.
Why is my delimiter not appearing in the final output? It's initialized to be a comma, but I only get ~5 white spaces between each attribute using:
SELECT [article_id]
, dbo.GROUP_CONCAT(0, t.tag_name, ',') AS col
FROM [AdventureWorks].[dbo].[ARTICLE_TAG_XREF] atx
JOIN [AdventureWorks].[dbo].[TAGS] t ON t.tag_id = atx.tag_id
GROUP BY article_id
The bit for DISTINCT works fine, but it operates within the Accumulate scope...
Output:
article_id | col
-------------------------------------------------
1 | a a b c
Update: The excess space between values is because the column as defined as NCHAR(10), so 10 characters would appear in the output. Silly mistake on my part...
Solution
With Martin Smith's help about working with the Write(BinaryWriter w) method, this update works for me:
public void Write(BinaryWriter w)
{
w.Write(list.Count);
for (int i = 0; i < list.Count; i++ )
{
if (i < list.Count - 1)
{
w.Write(list[i].ToString() + delimiter);
}
else
{
w.Write(list[i].ToString());
}
}
}
The Question:
Why does the above solve my problem? And why wouldn't it let me use more than one w.write call inside the FOR loop?
C# Code:
using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
using System.Xml.Serialization;
using System.Xml;
using System.IO;
using System.Collections;
using System.Text;
[Serializable]
[SqlUserDefinedAggregate(Format.UserDefined, MaxByteSize = 8000)]
public struct GROUP_CONCAT : IBinarySerialize
{
ArrayList list;
string delimiter;
public void Init()
{
list = new ArrayList();
delimiter = ",";
}
public void Accumulate(SqlBoolean isDistinct, SqlString Value, SqlString separator)
{
delimiter = (separator.IsNull) ? "," : separator.Value ;
if (!Value.IsNull)
{
if (isDistinct)
{
if (!list.Contains(Value.Value))
{
list.Add(Value.Value);
}
}
else
{
list.Add(Value.Value);
}
}
}
public void Merge(GROUP_CONCAT Group)
{
list.AddRange(Group.list);
}
public SqlString Terminate()
{
string[] strings = new string[list.Count];
for (int i = 0; i < list.Count; i++)
{
strings[i] = list[i].ToString();
}
return new SqlString(string.Join(delimiter, strings));
}
#region IBinarySerialize Members
public void Read(BinaryReader r)
{
int itemCount = r.ReadInt32();
list = new ArrayList(itemCount);
for (int i = 0; i < itemCount; i++)
{
this.list.Add(r.ReadString());
}
}
public void Write(BinaryWriter w)
{
w.Write(list.Count);
foreach (string s in list)
{
w.Write(s);
}
}
#endregion
}
The problem here is that you do not serialize delimiter. Add:
w.Write(delimiter)
as a first line in your Write method and
delimiter = r.ReadString();
as a first line in your Read method.
Regarding your questions to suggested work-around:
Why does the above solve my problem?
It does not. It merely worked with your test scenario.
And why wouldn't it let me use more than one w.write call inside the FOR loop?
Write method needs to be compatible with Read method. If you write two strings and read only one then it is not going to work. The idea here is that your object may be removed from the memory and then loaded. This is what Write and Read are supposed to do. In your case - this indeed was happening and you were not able to keep the object value.
The answer given by #agsamek is correct but not complete. The query processor may instantiate multiple aggregators, e.g. for parallel computations, and the one that will eventually hold all data after successive calls of Merge() may be assigned an empty recordset, i.e. its Accumulate() method may be never called:
var concat1 = new GROUP_CONCAT();
concat1.Init();
results = getPartialResults(1); // no records returned
foreach (var result in results)
concat1.Accumulate(result[0], delimiter); // never called
...
var concat2 = new GROUP_CONCAT();
concat2.Init();
results = getPartialResults(2);
foreach (var result in results)
concat2.Accumulate(result[0], delimiter);
...
concat1.Merge(concat2);
...
result = concat1.Terminate();
In this scenario, concat1's private field delimiter used in Terminate() remains what it is by default in Init() but not what you pass in SQL. Luckily or not, your test SQL uses the same delimiter value as in Init(), so you can't reveal the difference.
I'm not sure if this is a bug or if it has been fixed in later versions (I stumbled on it in SQL Server 2008 R2). My workaround was to make use of the other group that is passed in Merge():
public void Merge(GROUP_CONCAT Group)
{
if (Group.list.Count != 0) // Group's Accumulate() has been called at least once
{
if (list.Count == 0) // this Accumulate() has not been called
delimiter = Group.delimiter;
list.AddRange(Group.list);
}
}
P.S. I would use StringBuilder instead of ArrayList.