I've got a problem populating nested List<>
The object graph looks like this:
Route ⇒ Section ⇒ Co-ordinates
Whenever I try to populate Сoordinates list it just overwrites previous record and at the end gives me only the last Coordinate record. But I want all the Co-ordinates.
Here is my controller code:
List<RequestRouteDataClass> result = new List<RequestRouteDataClass> {
new RequestRouteDataClass() {
RouteRequestId = objRouteManagement.RouteRequestId,
RouteName = objRouteManagement.RouteName,
RouteDescription = objRouteManagement.RouteDescription,
RouteSections = new List<RouteSections> {
new RouteSections() {
Route_Sections_Id = objSections.Route_Sections_Id,
Section_Speed = objSections.Section_Speed,
Section_Description = objSections.Section_Description,
RouteCordinatesSections = new List<SectionCoordinatesRelationData> {
new SectionCoordinatesRelationData() {
SectionCoordinate_Relat_Id = objSectionsCordinates.SectionCoordinate_Relat_Id,
CoordinateLat = objSectionsCordinates.CoordinateLat,
CoordinateLag = objSectionsCordinates.CoordinateLag
}
}
}
}
}
If you want to use Nested List.
Your Model Contains =>
public class MainModelToUse
{
public MainModelToUse()
{
FirstListObject = new List<FirstListClass>();
}
public List<FirstListClass> FirstListObject { get; set; }
}
public class FirstListClass
{
public FirstListClass()
{
SecondListObject = new List<SecondListClass>();
}
public List<SecondListClass> SecondListObject { get; set; }
}
public class SecondListClass
{
public SecondListClass()
{
ThirdListObject = new List<ThirdListClass>();
}
public List<ThirdListClass> ThirdListObject { get; set; }
}
public class ThirdListClass
{
}
Your Code to Nested List =>
FirstListClass vmFirstClassMenu = new FirstListClass();
vmFirstClassMenu.SecondListClass = new List<SecondListClass>();
FirstListClass vmFirstClassCategory = new FirstListClass();
var dataObject1 = //Get Data By Query In Object;
foreach (Model objModel in dataObject1)
{
vmFirstClassCategory = new FirstListClass
{
//Your Items
};
var DataObject2 = //Get Data By Query In Object;
vmFirstClassCategory.SecondListClass = new List<SecondListClass>();
foreach (SecondListClass menuItem in DataObject2)
{
SecondListClass vmFirstClassMenuItem = new SecondListClass
{
//Your Items
};
var DataObject3 = //Get Data By Query In Object;
vmFirstClassMenuItem.ThirdListClass = new List<ThirdListClass>();
foreach (ThirdListClass price in DataObject3)
{
ThirdListClass vmThirdClassobj = new ThirdListClass
{
//Your Items
};
vmFirstClassMenuItem.ThirdListClass.Add(vmThirdClassobj);
}
vmFirstClassCategory.SecondListClass.Add(vmFirstClassMenuItem);
}
}
Hope this is what you are looking for.
First off: spacing helps with readability (edit: but I see you fixed that in your question already):
List<RequestRouteDataClass> result = new List<RequestRouteDataClass>
{
new RequestRouteDataClass()
{
RouteRequestId = objRouteManagement.RouteRequestId,
RouteName = objRouteManagement.RouteName,
RouteDescription = objRouteManagement.RouteDescription,
RouteSections = new List<RouteSections>
{
new RouteSections()
{
Route_Sections_Id = objSections.Route_Sections_Id,
Section_Speed = objSections.Section_Speed,
Section_Description = objSections.Section_Description,
RouteCordinatesSections = new List<SectionCoordinatesRelationData>
{
new SectionCoordinatesRelationData()
{
SectionCoordinate_Relat_Id = objSectionsCordinates.SectionCoordinate_Relat_Id,
CoordinateLat = objSectionsCordinates.CoordinateLat,
CoordinateLag =objSectionsCordinates.CoordinateLag
}
}
}
}
}
};
Next: what you are doing with the above is initiating your lists with a single element in each list. If you want more elements, you have to add them. I recommend using a foreach and the Add() functionality to fill your lists.
From your example it is not clear how your source data is stored, but if you have multiples of something I would expect those too to be in a list or an array of some kind.
Related
I have made a small class, which inherits from DataGrid and takes in classes that derive from a specific interface:
public class RecordDataGrid<T> : DataGrid where T : IRecord
{
public RecordDataGrid()
{
this.AutoGenerateColumns = false;
this.CanUserAddRows = false;
this.CanUserDeleteRows = false;
this.CanUserResizeRows = false;
this.IsReadOnly = true;
this.SelectionMode = DataGridSelectionMode.Single;
this.Margin = new System.Windows.Thickness(0, 10, 0, 0);
var propertyInfos = typeof(T).GetProperties();
var list = new Dictionary<PropertyInfo, DataGridColumnAttribute>();
foreach (var propertyInfo in propertyInfos)
{
var customAttributes = propertyInfo.GetCustomAttributes(true);
foreach (var customAttr in customAttributes)
{
if (customAttr != null && customAttr is DataGridColumnAttribute)
{
list.Add(propertyInfo, (DataGridColumnAttribute)customAttr);
}
}
}
var ordered = (from entry in list orderby entry.Value.OrderIndex ascending select entry).ToDictionary(e => e.Key, e => e.Value);
foreach (var kvp in ordered)
{
var propertyInfo = kvp.Key;
var dgcAttr = kvp.Value;
var column = new DataGridTextColumn();
column.Header = dgcAttr.DisplayName;
column.Binding = new Binding(propertyInfo.Name);
column.Binding.StringFormat = dgcAttr.StringFormat ?? null;
column.Width = dgcAttr.ColumnWidthType == DataGridColumnAttribute.ColumnWidthTypes.Auto ? new DataGridLength(10, DataGridLengthUnitType.Auto) : new DataGridLength(10, DataGridLengthUnitType.Star);
this.Columns.Add(column);
}
}
}
It is very rough at the moment, just testing a few things out. The goal is to make my life easier by letting the DataGrid fill the Columns by itself, based on a custom Attribute:
public class DataGridColumnAttribute : Attribute
{
public string DisplayName { get; private set; }
public string StringFormat { get; private set; }
public ColumnWidthTypes ColumnWidthType { get; private set; }
public int OrderIndex { get; private set; }
public DataGridColumnAttribute(string displayName, int orderIndex, string stringFormat = null, ColumnWidthTypes columnWidthType = ColumnWidthTypes.Auto)
{
DisplayName = displayName;
StringFormat = stringFormat;
OrderIndex = OrderIndex;
ColumnWidthType = columnWidthType;
}
public enum ColumnWidthTypes
{
Auto,
Fill
}
}
Later on, as far as I am concerned, I should be able to use it in xaml like this:
Namespaces:
xmlns:model="clr-namespace:NickX.KswErp.Model.Classes;assembly=NickX.KswErp.Model"
xmlns:ctrl="clr-namespace:NickX.KswErp.ClientApplication.UI.Controls"
Control:
<ctrl:RecordDataGrid x:Name="_gridTransactions" x:TypeArguments="model:TransactionRecord" />
But I get following compilation error:
Only a master tag can specify the "x: TypeArguments" attribute.
(Roughly translated by google translation)
Maybe my approach is completely wrong tho. Should I do it completle in code behind. Or are there better approaches? Please let me know!
Conveniently I just found a thread in a german forum, which answeres my exact question. So people questioning the same in the future:
It is not possible. Easiest thing to do at this point is making a specific class for each model, which again derives from your generic class.
In my case:
public class TransactionDataGrid : RecordDataGrid<TransactionRecord>
{
}
Doesen't seem like a nice solution to me, and probably isn't the best way to do it. But it works.
I am using cosmos db to store and fetch data. Previously I was using DocumentClient like:
public class ProductRepository : IProductRepository
{
private DocumentClient _documentClient;
private DocumentCollection _graphCollection;
public ProductRepository(DocumentClient documentClient, DocumentCollection graphCollection)
{
_documentClient = documentClient;
_graphCollection = graphCollection;
}
public async Task Create(Product product)
{
var createQuery = CreateQuery(product);
IDocumentQuery<dynamic> query = _documentClient.CreateGremlinQuery<dynamic>(_graphCollection, createQuery);
if(query.HasMoreResults)
{
await query.ExecuteNextAsync();
}
}
public async Task<Product> Get(string id)
{
Product product = null;
var getQuery = #"g.V('" + id + "')";
var query = _documentClient.CreateGremlinQuery<dynamic>(_graphCollection, getQuery);
if (query.HasMoreResults)
{
var result = await query.ExecuteNextAsync();
if (result.Count == 0)
return product;
var productData = (JObject)result.FirstOrDefault();
product = new Product
{
name = productData["name"].ToString()
};
}
return product;
}
}
}
But it is not unit testable so I want to convert it to IDocumentClient but IDocumentClient doesn't contain definition for CreateGremlinQuery. So what is the best possible way to convert my methods so that they will be using IDocumentClient? Do I need to use CreateDocumentQuery? if yes, how can I convert CreateGremlimQuery to CreateDocumentQuery?
There are several ways to get around that. The simplest one would be to simply hard cast your IDocumentClient to DocumentClient.
If you go with that approach your code becomes:
public class ProductRepository : IProductRepository
{
private IDocumentClient _documentClient;
private DocumentCollection _graphCollection;
public ProductRepository(IDocumentClient documentClient, DocumentCollection graphCollection)
{
_documentClient = documentClient;
_graphCollection = graphCollection;
}
public async Task Create(Product product)
{
var createQuery = CreateQuery(product);
IDocumentQuery<dynamic> query = ((DocumentClient)_documentClient).CreateGremlinQuery<dynamic>(_graphCollection, createQuery);
if(query.HasMoreResults)
{
await query.ExecuteNextAsync();
}
}
public async Task<Product> Get(string id)
{
Product product = null;
var getQuery = #"g.V('" + id + "')";
var query = ((DocumentClient)_documentClient).CreateGremlinQuery<dynamic>(_graphCollection, getQuery);
if (query.HasMoreResults)
{
var result = await query.ExecuteNextAsync();
if (result.Count == 0)
return product;
var productData = (JObject)result.FirstOrDefault();
product = new Product
{
name = productData["name"].ToString()
};
}
return product;
}
}
You could also create your own extensions for IDocumentClient.
public static class MoreGraphExtensions
{
public static IDocumentQuery<T> CreateGremlinQuery<T>(this IDocumentClient documentClient, DocumentCollection collection, string gremlinExpression, FeedOptions feedOptions = null, GraphSONMode graphSONMode = GraphSONMode.Compact)
{
return GraphExtensions.CreateGremlinQuery<T>((DocumentClient)documentClient, collection, gremlinExpression, feedOptions, graphSONMode);
}
public static IDocumentQuery<object> CreateGremlinQuery(this IDocumentClient documentClient, DocumentCollection collection, string gremlinExpression, FeedOptions feedOptions = null, GraphSONMode graphSONMode = GraphSONMode.Compact)
{
return GraphExtensions.CreateGremlinQuery<object>((DocumentClient)documentClient, collection, gremlinExpression, feedOptions, graphSONMode);
}
}
It is a pre-release however, so I do think that Microsoft will get around moving the extension methods at the interface level.
I am creating an application with MVC4 and entity framework 5. How do can I implement this?
I have looked around and found that I need to override SaveChanges .
Does anyone have any sample code on this? I am using code first approach.
As an example, the way I am saving data is as follows,
public class AuditZoneRepository : IAuditZoneRepository
{
private AISDbContext context = new AISDbContext();
public int Save(AuditZone model, ModelStateDictionary modelState)
{
if (model.Id == 0)
{
context.AuditZones.Add(model);
}
else
{
var recordToUpdate = context.AuditZones.FirstOrDefault(x => x.Id == model.Id);
if (recordToUpdate != null)
{
recordToUpdate.Description = model.Description;
recordToUpdate.Valid = model.Valid;
recordToUpdate.ModifiedDate = DateTime.Now;
}
}
try
{
context.SaveChanges();
return 1;
}
catch (Exception ex)
{
modelState.AddModelError("", "Database error has occured. Please try again later");
return -1;
}
}
}
There is no need to override SaveChanges.
You can
Trigger Context.ChangeTracker.DetectChanges(); // may be necessary depending on your Proxy approach
Then analyze the context BEFORE save.
you can then... add the Change Log to the CURRENT Unit of work.
So the log gets saved in one COMMIT transaction.
Or process it as you see fit.
But saving your change log at same time. makes sure it is ONE Transaction.
Analyzing the context sample:
I have a simple tool, to Dump context content to debug output so when in debugger I can use immediate window to check content. eg
You can use this as a starter to prepare your CHANGE Log.
Try it in debugger immediate window. I have FULL dump on my Context class.
Sample Immediate window call. UoW.Context.FullDump();
public void FullDump()
{
Debug.WriteLine("=====Begin of Context Dump=======");
var dbsetList = this.ChangeTracker.Entries();
foreach (var dbEntityEntry in dbsetList)
{
Debug.WriteLine(dbEntityEntry.Entity.GetType().Name + " => " + dbEntityEntry.State);
switch (dbEntityEntry.State)
{
case EntityState.Detached:
case EntityState.Unchanged:
case EntityState.Added:
case EntityState.Modified:
WriteCurrentValues(dbEntityEntry);
break;
case EntityState.Deleted:
WriteOriginalValues(dbEntityEntry);
break;
default:
throw new ArgumentOutOfRangeException();
}
Debug.WriteLine("==========End of Entity======");
}
Debug.WriteLine("==========End of Context======");
}
private static void WriteCurrentValues(DbEntityEntry dbEntityEntry)
{
foreach (var cv in dbEntityEntry.CurrentValues.PropertyNames)
{
Debug.WriteLine(cv + "=" + dbEntityEntry.CurrentValues[cv]);
}
}
private static void WriteOriginalValues(DbEntityEntry dbEntityEntry)
{
foreach (var cv in dbEntityEntry.OriginalValues.PropertyNames)
{
Debug.WriteLine(cv + "=" + dbEntityEntry.OriginalValues[cv]);
}
}
}
EDIT: Get the changes
I use this routine to get chnages...
public class ObjectPair {
public string Key { get; set; }
public object Original { get; set; }
public object Current { get; set; }
}
public virtual IList<ObjectPair> GetChanges(object poco) {
var changes = new List<ObjectPair>();
var thePoco = (TPoco) poco;
foreach (var propName in Entry(thePoco).CurrentValues.PropertyNames) {
var curr = Entry(thePoco).CurrentValues[propName];
var orig = Entry(thePoco).OriginalValues[propName];
if (curr != null && orig != null) {
if (curr.Equals(orig)) {
continue;
}
}
if (curr == null && orig == null) {
continue;
}
var aChangePair = new ObjectPair {Key = propName, Current = curr, Original = orig};
changes.Add(aChangePair);
}
return changes;
}
edit 2 If you must use the Internal Object tracking.
var context = ???// YOUR DBCONTEXT class
// get objectcontext from dbcontext...
var objectContext = ((IObjectContextAdapter) context).ObjectContext;
// for each tracked entry
foreach (var dbEntityEntry in context.ChangeTracker.Entries()) {
//get the state entry from the statemanager per changed object
var stateEntry = objectContext.ObjectStateManager.GetObjectStateEntry(dbEntityEntry.Entity);
var modProps = stateEntry.GetModifiedProperties();
Debug.WriteLine(modProps.ToString());
}
I decompiled EF6 . Get modified is indeed using private bit array to track fields that have
been changed.
// EF decompiled source..... _modifiedFields is a bitarray
public override IEnumerable<string> GetModifiedProperties()
{
this.ValidateState();
if (EntityState.Modified == this.State && this._modifiedFields != null)
{
for (int i = 0; i < this._modifiedFields.Length; ++i)
{
if (this._modifiedFields[i])
yield return this.GetCLayerName(i, this._cacheTypeMetadata);
}
}
}
I have my web api and I added the web api help pages to auto-generate my documentation. It's working great for methods where my parameters are listed out, but I have a method like this:
public SessionResult PostLogin(CreateSessionCommand request)
And, on my help page, it is only listing the command parameter in the properties section. However, in the sample request section, it lists out all of the properties of my CreateSessionCommand class.
Parameters
Name | Description | Additional information
request | No documentation available. | Define this parameter in the request body.
I would like it instead to list all of the properties in my CreateSessionCommand class. Is there an easy way to do this?
So, I managed to devise a workaround for this problem, in case anyone is interested.
In HelpPageConfigurationExtensions.cs I added the following extension method:
public static void AlterApiDescription(this ApiDescription apiDescription, HttpConfiguration config)
{
var docProvider = config.Services.GetDocumentationProvider();
var addParams = new List<ApiParameterDescription>();
var removeParams = new List<ApiParameterDescription>();
foreach (var param in apiDescription.ParameterDescriptions)
{
var type = param.ParameterDescriptor.ParameterType;
//string is some special case that is not a primitive type
//also, compare by full name because the type returned does not seem to match the types generated by typeof
bool isPrimitive = type.IsPrimitive || String.Compare(type.FullName, typeof(string).FullName) == 0;
if (!isPrimitive)
{
var properties = from p in param.ParameterDescriptor.ParameterType.GetProperties()
let s = p.SetMethod
where s.IsPublic
select p;
foreach (var property in properties)
{
var documentation = docProvider.GetDocumentation(new System.Web.Http.Controllers.ReflectedHttpParameterDescriptor()
{
ActionDescriptor = param.ParameterDescriptor.ActionDescriptor,
ParameterInfo = new CustomParameterInfo(property)
});
addParams.Add(new ApiParameterDescription()
{
Documentation = documentation,
Name = property.Name,
Source = ApiParameterSource.FromBody,
ParameterDescriptor = param.ParameterDescriptor
});
}
//since this is a complex type, select it to be removed from the api description
removeParams.Add(param);
}
}
//add in our new items
foreach (var item in addParams)
{
apiDescription.ParameterDescriptions.Add(item);
}
//remove the complex types
foreach (var item in removeParams)
{
apiDescription.ParameterDescriptions.Remove(item);
}
}
And here is the Parameter info instanced class I use
internal class CustomParameterInfo : ParameterInfo
{
public CustomParameterInfo(PropertyInfo prop)
{
base.NameImpl = prop.Name;
}
}
Then, we call the extension in another method inside the extensions class
public static HelpPageApiModel GetHelpPageApiModel(this HttpConfiguration config, string apiDescriptionId)
{
object model;
string modelId = ApiModelPrefix + apiDescriptionId;
if (!config.Properties.TryGetValue(modelId, out model))
{
Collection<ApiDescription> apiDescriptions = config.Services.GetApiExplorer().ApiDescriptions;
ApiDescription apiDescription = apiDescriptions.FirstOrDefault(api => String.Equals(api.GetFriendlyId(), apiDescriptionId, StringComparison.OrdinalIgnoreCase));
if (apiDescription != null)
{
apiDescription.AlterApiDescription(config);
HelpPageSampleGenerator sampleGenerator = config.GetHelpPageSampleGenerator();
model = GenerateApiModel(apiDescription, sampleGenerator);
config.Properties.TryAdd(modelId, model);
}
}
return (HelpPageApiModel)model;
}
The comments that are used for this must be added to the controller method and not the properties of the class object. This might be because my object is part of an outside library
this should go as an addition to #Josh answer. If you want not only to list properties from the model class, but also include documentation for each property, Areas/HelpPage/XmlDocumentationProvider.cs file should be modified as follows:
public virtual string GetDocumentation(HttpParameterDescriptor parameterDescriptor)
{
ReflectedHttpParameterDescriptor reflectedParameterDescriptor = parameterDescriptor as ReflectedHttpParameterDescriptor;
if (reflectedParameterDescriptor != null)
{
if (reflectedParameterDescriptor.ParameterInfo is CustomParameterInfo)
{
const string PropertyExpression = "/doc/members/member[#name='P:{0}']";
var pi = (CustomParameterInfo) reflectedParameterDescriptor.ParameterInfo;
string selectExpression = String.Format(CultureInfo.InvariantCulture, PropertyExpression, pi.Prop.DeclaringType.FullName + "." + pi.Prop.Name);
XPathNavigator methodNode = _documentNavigator.SelectSingleNode(selectExpression);
if (methodNode != null)
{
return methodNode.Value.Trim();
}
}
else
{
XPathNavigator methodNode = GetMethodNode(reflectedParameterDescriptor.ActionDescriptor);
if (methodNode != null)
{
string parameterName = reflectedParameterDescriptor.ParameterInfo.Name;
XPathNavigator parameterNode = methodNode.SelectSingleNode(String.Format(CultureInfo.InvariantCulture, ParameterExpression, parameterName));
if (parameterNode != null)
{
return parameterNode.Value.Trim();
}
}
}
}
return null;
}
and CustomParameterInfo class should keep property info as well:
internal class CustomParameterInfo : ParameterInfo
{
public PropertyInfo Prop { get; private set; }
public CustomParameterInfo(PropertyInfo prop)
{
Prop = prop;
base.NameImpl = prop.Name;
}
}
This is currently not supported out of the box. Following bug is kind of related to that:
http://aspnetwebstack.codeplex.com/workitem/877
thanks for taking the time out to read this post.
I'm having trouble trying to build a hierarchial object when getting data from my SQL database.
Please note that I am a little bit of a newbie programmer.
How do you build a hierarchial object that has unknown levels? When I say unknown levels I mean, each node may have varying numbers of child nodes, which in turn may have varying numbers of its own child nodes, so on and so on.
The idea is that I need to create a hierarchial object using my SQL data to bind to WPF TreeView control.
Below I have included the code I have so far.
The first bit of code is my Class made up of Properties. Note that the "Products" class has an ObservableCollection referencing itself. I think this is how you construct the nested nodes. i.e. a list inside a list.
The second piece of code is my Get Method to download the data from the SQL database. Here is where I need to some how sort the downloaded data into a hierarchy.
Products Class (properties)
public class Products : INotifyPropertyChanged, IDataErrorInfo
{
private Int64 m_ID;
private SqlHierarchyId m_Hierarchy;
private string m_Name;
private ObservableCollection<Products> m_ChildProducts;
// Default Constructor
public Products()
{
ChildProducts = new ObservableCollection<Products>();
}
//Properties
public Int64 ID
{
get
{
return m_ID;
}
set
{
m_ID = value;
OnPropertyChanged(new PropertyChangedEventArgs("ID"));
}
}
public SqlHierarchyId Hierarchy
{
get
{
return m_Hierarchy;
}
set
{
m_Hierarchy = value;
OnPropertyChanged(new PropertyChangedEventArgs("Hierarchy"));
}
}
public String Name
{
get
{
return m_Name;
}
set
{
m_Name = value;
OnPropertyChanged(new PropertyChangedEventArgs("Name"));
}
}
public Int16 Level
{
get
{
return m_Level;
}
set
{
m_Level = value;
OnPropertyChanged(new PropertyChangedEventArgs("Level"));
}
}
public Int64 ParentID
{
get
{
return m_ParentID;
}
set
{
m_ParentID = value;
OnPropertyChanged(new PropertyChangedEventArgs("ParentID"));
}
}
public ObservableCollection<Products> ChildProducts
{
get
{
return m_ChildProducts;
}
set
{
m_ChildProducts = value;
OnPropertyChanged(new PropertyChangedEventArgs("ChildProducts"));
}
}
//INotifyPropertyChanged Event
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null)
PropertyChanged(this, e);
}
}
Method which gets data from SQL DB:
public static ObservableCollection<Products> GetProductsHierarchy()
{
ObservableCollection<Products> productsHierarchy = new ObservableCollection<Products>();
SqlConnection connection = new SqlConnection(DBConnection.GetConnection().ConnectionString);
string selectStatement = "SELECT ID, Hierarchy, Name, Hierarchy.GetLevel() AS Level, Hierarchy.GetAncestor(1) AS ParentHierarchy, " +
"(SELECT ID " +
"FROM SpecProducts " +
"WHERE (Hierarchy = SpecProducts_1.Hierarchy.GetAncestor(1))) AS ParentID " +
"FROM SpecProducts AS SpecProducts_1 " +
"WHERE (EnableDisable IS NULL) " +
"ORDER BY Hierarchy";
SqlCommand selectCommand = new SqlCommand(selectStatement, connection);
try
{
connection.Open();
SqlDataReader reader = selectCommand.ExecuteReader();
while (reader.Read())
{
Products product = new Products();
product.ID = (Int64)reader["ID"];
product.Name = reader["Name"].ToString();
product.Hierarchy = (SqlHierarchyId)reader["Hierarchy"];
product.Level = (Int16)reader["Level"];
if (reader["ParentID"] != DBNull.Value)
{
product.ParentID = (Int64)reader["ParentID"];
}
else
{
product.ParentID = 0;
}
productsHierarchy.Add(product);
// *** HOW TO BUILD HIERARCHY OBJECT WITH UNKNOWN & VARYING LEVELS?
// *** ADD PRODUCT TO CHILDPRODUCT
}
return productsHierarchy;
}
catch (SqlException ex)
{
throw ex;
}
finally
{
connection.Close();
}
}
Below I have attached an image showing the structure of my SQL Query Data.
Please note that the hierarchy level may go deeper when more products are added in the future. The Hierarchy object I need to create should be flexible enough to expand no matter what the number of node levels are.
Thank you very much for your time, all help is greatly appreciated.
********* EDIT 26/04/2012 14:37 *******************
Please find below a link to download my project code (this only contains treeview code).
Can someone please take a look at it to see why I cannot create nodes beyond 2 levels?
The code was given to me by user HB MAAM. Thank you "HB MAAM" for your help so far!
Click this link to download code
I will create an example for you,
1- first i will create a class that holds the data that comes from the DB
public class SqlDataDto
{
public int? Id { get; set; }
public int? ParentId { get; set; }
public String Name { get; set; }
public String OtherDataRelatedToTheNode { get; set; }
}
2- that data will be converted to hierarchal data and we will use this class to hold it:
public class LocalData : INotifyPropertyChanged
{
private int? _id;
public int? Id
{
get { return _id; }
set { _id = value; OnPropertyChanged("Id"); }
}
private int? _parentId;
public int? ParentId
{
get { return _parentId; }
set { _parentId = value; OnPropertyChanged("ParentId"); }
}
private string _name;
public String Name
{
get { return _name; }
set { _name = value; OnPropertyChanged("Name"); }
}
private string _otherDataRelatedToTheNode;
public String OtherDataRelatedToTheNode
{
get { return _otherDataRelatedToTheNode; }
set { _otherDataRelatedToTheNode = value; OnPropertyChanged("OtherDataRelatedToTheNode"); }
}
private LocalData _parent;
public LocalData Parent
{
get { return _parent; }
set { _parent = value; OnPropertyChanged("Parent"); }
}
private ObservableCollection<LocalData> _children;
public ObservableCollection<LocalData> Children
{
get { return _children; }
set { _children = value; OnPropertyChanged("Children"); }
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(String propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this,new PropertyChangedEventArgs(propertyName));
}
}
}
3- finally we need to change the sql data to hierarchical one:
public List<LocalData> GetHerachy(List<SqlDataDto> sqlData)
{
var sqlParents = sqlData.Where(q => q.ParentId == null).ToList();
var parents = sqlParents.Select(q => new LocalData {Id = q.Id, Name = q.Name}).ToList();
foreach (var parent in parents)
{
var childs = sqlData.Where(q => q.ParentId == parent.Id).Select(q => new LocalData { Id = q.Id, Name = q.Name , Parent = parent});
parent.Children = new ObservableCollection<LocalData>(childs);
}
return parents;
}
4- then you can create a dummy data and convert it and show it in the tree:
var sqlData = new List<SqlDataDto>
{
new SqlDataDto {Id = 1, ParentId = null, Name = "F1"}
, new SqlDataDto {Id = 2, ParentId = null, Name = "F2"}
, new SqlDataDto {Id = 3, ParentId = 1, Name = "S1"}
, new SqlDataDto {Id = 4, ParentId = 2, Name = "S21"}
, new SqlDataDto {Id = 5, ParentId = 2, Name = "S22"}
};
treeView.ItemsSource = GetHerachy(sqlData);
5- the tree should be like:
<TreeView Name="treeView">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Name}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
You need to use recursion to fill the Child-List of every object. This is necessary for the WPF HierarchicalDataTemplate to work. Otherwise you only get the first level.
There is an alternative using the Linq method ForEach() and passing an Action Argument. The following solution is very straight forward and easy to understand:
public List<Product> Products { get; set; }
public MainViewModel()
{
Products = new List<Product>();
Products.Add(new Product() { Id = 1, Name = "Main Product 1", ParentId = 0 });
Products.Add(new Product() { Id = 3, Name = "Sub Product 1", ParentId = 1 });
Products.Add(new Product() { Id = 4, Name = "Sub Product 2", ParentId = 1 });
Products.Add(new Product() { Id = 5, Name = "Sub Product 3", ParentId = 1 });
Products.Add(new Product() { Id = 6, Name = "Sub Product 3.1", ParentId = 5 });
this.ProcessRootNodes();
}
private void ProcessRootNodes()
{
var rootNodes = Products.Where(x => x.ParentId == 0).ToList();
for (int i = 0; i < rootNodes.Count; i++)
{
rootNodes[i].Children = this.AddChildren(rootNodes[i]);
}
}
private List<Product> AddChildren(Product entry)
{
var children = Products.Where(x => x.ParentId == entry.Id).ToList();
for(int i=0;i<children.Count;i++)
{
children[i].Children = this.AddChildren(children[i]);
}
return children;
}
// *** HOW TO BUILD HIERARCHY OBJECT WITH UNKNOWN & VARYING LEVELS?
Instead of
ObservableCollection<Products> productsHierarchy = new ObservableCollection<Products>();
use Dictionary<Int64, Products> IdToProduct = new ...
As you loop your products; do a IdToProduct[product.ID] = product;
Then, loop the completed IdToProduct collection and do;
if(product.ParentID != 0)
{
IdToProduct[product.ParentID].ChildProducts.Add(product);
}
Now, your Product --> ChildProducts relation is mapped out.
Optionally, add properties to the Products class:
public bool IsCategory { get { return (ChildProducts.Count >= 1); } } // e.g. Oven
public bool IsProduct { get { return !(IsCategory); } } // e.g. Electric (Oven)
Now, you have most of the model for your view defined.
This article is the de facto starting point for using the WPF TreeView.
Hint: a starting point for your HierarchicalDataTemplate
<TreeView.ItemTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:Products}"
ItemsSource="{Binding ChildProducts}">
<TextBlock Text="{Binding Name}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
You should create a MainViewModel class which has:
public Products RootProduct { get; set; } (notify property changed property)
after you do your SQL parsing and what not; do:
RootProduct = IdToProduct.FirstOrDefault(product => (product.Level == 0));
<TreeView ItemsSource="{Binding RootProduct.ChildProducts}">