How can I correlate events in a masstransit state machine without using a Guid? - masstransit

I have defined the following state machine in Masstransit:
public class OrderStateMachine : MassTransitStateMachine<OrderState>
{
public OrderStateMachine()
{
InstanceState(x => x.Status);
Event(() => OrderCreated, x => x.CorrelateBy(order => order.OrderCode, ctx => ctx.Message.OrderCode).SelectId(ctx => NewId.NextGuid()));
//How should I select an id for these events?
Event(() => OrderProvisioned, x => x.CorrelateBy(order => order.OrderCode, ctx => ctx.Message.OrderCode));
Event(() => OrderInvoiced, x => x.CorrelateBy(order => order.OrderCode, ctx => ctx.Message.OrderCode));
State(() => Created);
State(() => Finished);
CompositeEvent(() => OrderFinished, order => order.CompositeStatus, OrderProvisioned, OrderInvoiced);
Initially(
When(OrderCreated)
.Then(context => Console.WriteLine("Order created"))
.TransitionTo(Created));
During(Created,
When(OrderFinished)
.Then(context => Console.WriteLine("Order finished"))
.TransitionTo(Finished)
.Finalize());
}
public State Created { get; set; }
public State Finished { get; set; }
public Event<OrderCreated> OrderCreated { get; set; }
public Event<OrderProvisioned> OrderProvisioned { get; set; }
public Event<OrderInvoiced> OrderInvoiced { get; set; }
public Event OrderFinished { get; set; }
}
public class OrderState : SagaStateMachineInstance
{
public Guid CorrelationId { get; set; }
public string OrderCode { get; set; }
public string Status { get; set; }
public CompositeEventStatus CompositeStatus { get; set; }
}
public class OrderCreated
{
public string OrderCode { get; set; }
public OrderCreated(string orderCode)
{
OrderCode = orderCode;
}
}
public class OrderInvoiced
{
public string OrderCode { get; set; }
public OrderInvoiced(string orderCode)
{
OrderCode = orderCode;
}
}
public class OrderProvisioned
{
public string OrderCode { get; set; }
public OrderProvisioned(string orderCode)
{
OrderCode = orderCode;
}
}
How can I correlate the OrderProvisoned and the OrderInvoiced event to the same OrderState instance as the initial OrderCreated event without sending Guids in my events and only use the ordercode property to correlate them?
If I run this example, I never get the OrderFinished event if both OrderProvisioned and OrderInvoiced are sent, but if I add Guids to the events and correlate them based on that Guid it is executed correctly.

I solved it, apparently you must explicitly set the custom correlationid on the statemachine instance, I would expect that masstransit would do that for me.
So this is the adapted code for the initial state:
Initially(
When(OrderCreated)
.Then(context => context.Instance.OrderCode = context.Data.OrderCode)
.Then(context => Console.WriteLine("Order created"))
.TransitionTo(Created));
Also using a saga factory works, but I would think that you could drop the selectid logic as the saga factory overrules this, but than I get an exception.
Event(() => OrderCreated,
x =>
{
x.CorrelateBy(order => order.OrderCode, ctx => ctx.Message.OrderCode);
x.InsertOnInitial = true;
x.SetSagaFactory(context => new OrderState
{
CorrelationId = NewId.NextGuid(),
OrderCode = context.Message.OrderCode
});
x.SelectId(context => NewId.NextGuid());
});

Is there a broken unit test that would be usable to see if this behavior is not working? From the edited code above, that seems to be how I would expect it to be correlated - using the OrderCode. That's how the shopping cart example works as well.
The SelectId() function is only needed for an event that initiates the state machine, since that is where the CorrelationId is assigned. If it isn't used by the other events, there is no need to "select" it since the Query (the orderCode) would do the correlation.
That's how the shopping cart example works in the sample:
https://github.com/MassTransit/Sample-ShoppingWeb/blob/master/src/CartTracking/ShoppingCartStateMachine.cs#L15
In your example above:
Event(() => OrderCreated, x => x.CorrelateBy(order => order.OrderCode, ctx => ctx.Message.OrderCode).SelectId(ctx => NewId.NextGuid()));
Event(() => OrderProvisioned, x => x.CorrelateBy(order => order.OrderCode, ctx => ctx.Message.OrderCode));
Event(() => OrderInvoiced, x => x.CorrelateBy(order => order.OrderCode, ctx => ctx.Message.OrderCode));
This is exactly how I would configure it, and it seems like you're on the right track.

Related

EntityFrameworkCore throws NullReferenceException when querying data

I'm using EF Core 2.2 to connect to an Oracle11g database (using these old versions is a must), when I try to query one of my tables it throws a NullReferenceException in the EF Core external code.
The Query:
string username = "Administrator";
var user = _context.Users.Where(x => x.UserName.ToUpper() == username.ToUpper())
.Include(x => x.UserProfile)
.Include(x => x.UserRoleOrganizations).ThenInclude(s => s.Role)
.Include(x => x.UserRoleOrganizations).ThenInclude(s => s.Organization);
return user.FirstOrDefault();
The exception:
System.NullReferenceException
HResult=0x80004003
Message=Object reference not set to an instance of an object.
Source=Microsoft.EntityFrameworkCore.Relational
StackTrace:
at Microsoft.EntityFrameworkCore.Storage.TypedRelationalValueBufferFactoryFactory.CreateGetValueExpression(Expression dataReaderExpression, Int32 index, TypeMaterializationInfo materializationInfo, Boolean detailedErrorsEnabled, Boolean box)
at Microsoft.EntityFrameworkCore.Storage.TypedRelationalValueBufferFactoryFactory.<>c__DisplayClass13_0.<CreateArrayInitializer>b__0(TypeMaterializationInfo mi, Int32 i)
at System.Linq.Enumerable.<SelectIterator>d__5`2.MoveNext()
at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
at System.Dynamic.Utils.CollectionExtensions.ToReadOnly[T](IEnumerable`1 enumerable)
at System.Linq.Expressions.Expression.NewArrayInit(Type type, IEnumerable`1 initializers)
at Microsoft.EntityFrameworkCore.Storage.TypedRelationalValueBufferFactoryFactory.CreateArrayInitializer(CacheKey cacheKey, Boolean detailedErrorsEnabled)
at Microsoft.EntityFrameworkCore.Storage.TypedRelationalValueBufferFactoryFactory.<Create>b__11_0(CacheKey k)
at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
at Microsoft.EntityFrameworkCore.Internal.NonCapturingLazyInitializer.EnsureInitialized[TParam,TValue](TValue& target, TParam param, Func`2 valueFactory)
at Microsoft.EntityFrameworkCore.Query.Internal.ShaperCommandContext.NotifyReaderCreated(DbDataReader dataReader)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryingEnumerable`1.Enumerator.BufferlessMoveNext(DbContext _, Boolean buffer)
at Oracle.EntityFrameworkCore.Storage.Internal.OracleExecutionStrategy.Execute[TState,TResult](TState state, Func`3 operation, Func`3 verifySucceeded)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryingEnumerable`1.Enumerator.MoveNext()
at System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext()
at System.Linq.Enumerable.FirstOrDefault[TSource](IEnumerable`1 source)
at Microsoft.EntityFrameworkCore.Query.Internal.LinqOperatorProvider.ResultEnumerable`1.GetEnumerator()
at Microsoft.EntityFrameworkCore.Query.Internal.LinqOperatorProvider.<_TrackEntities>d__17`2.MoveNext()
at Microsoft.EntityFrameworkCore.Query.Internal.LinqOperatorProvider.ExceptionInterceptor`1.EnumeratorExceptionInterceptor.MoveNext()
at System.Linq.Enumerable.First[TSource](IEnumerable`1 source)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<>c__DisplayClass15_1`1.<CompileQueryCore>b__0(QueryContext qc)
at Base.Security.Business.UserManagement.GetUser(String username) in C:\Users\Afagh2\source\repos\HSE2_Oracle\Base.Security\Business\UserManagement.cs:line 88
The generated query:
[Parameters=[:ToUpper_0='ADMINISTRATOR' (Size = 256)], CommandType='Text', CommandTimeout='0']
Select
K0 "Id", K1 "AccessFailedCount", K2 "ConcurrencyStamp", K3 "Email", K4 "EmailConfirmed", K5 "FirstName", K6 "IsActive", K7 "IsExternal", K8 "LastName", K9 "LockoutEnabled", K10 "LockoutEnd", K11 "NationalCode", K12 "NormalizedEmail", K13 "NormalizedUserName", K14 "OrganizationName", K15 "OrganizationType", K16 "PasswordHash", K17 "PersonnelCode", K18 "PhoneNumber", K19 "PhoneNumberConfirmed", K20 "PositionName", K21 "SecurityStamp", K22 "TwoFactorEnabled", K23 "UserName", K24 "Id", K25 "FileContent", K26 "FileExtension", K27 "FileName" from(
SELECT "x"."Id" K0, "x"."AccessFailedCount" K1, "x"."ConcurrencyStamp" K2, "x"."Email" K3, "x"."EmailConfirmed" K4, "x"."FirstName" K5, "x"."IsActive" K6, "x"."IsExternal" K7, "x"."LastName" K8, "x"."LockoutEnabled" K9, "x"."LockoutEnd" K10, "x"."NationalCode" K11, "x"."NormalizedEmail" K12, "x"."NormalizedUserName" K13, "x"."OrganizationName" K14, "x"."OrganizationType" K15, "x"."PasswordHash" K16, "x"."PersonnelCode" K17, "x"."PhoneNumber" K18, "x"."PhoneNumberConfirmed" K19, "x"."PositionName" K20, "x"."SecurityStamp" K21, "x"."TwoFactorEnabled" K22, "x"."UserName" K23, "x.UserProfile"."Id" K24, "x.UserProfile"."FileContent" K25, "x.UserProfile"."FileExtension" K26, "x.UserProfile"."FileName" K27
FROM "AppUser" "x"
LEFT JOIN "AppUserProfilePicture" "x.UserProfile" ON ("x"."Id" = "x.UserProfile"."Id")
WHERE ("UPPER"("x"."UserName") = :ToUpper_0)
ORDER BY "x"."Id"
) "m1"
where rownum <= 1
The query works as expected in Oracle SQL Developer. It seems that EF Core has some trouble converting the results to the entity (which in this case, the result should be none because the table has no data.) but I can not debug its code. (Or can I?)
The entity is configured in OnModelCreating:
builder.Entity<ApplicationUser>(b =>
{
b.HasKey(u => u.Id);
b.HasIndex(u => u.NormalizedUserName).HasName("UserNameIndex").IsUnique();
b.HasIndex(u => u.NormalizedEmail).HasName("EmailIndex");
b.ToTable("AppUser");
b.Property(u => u.ConcurrencyStamp).IsConcurrencyToken();
b.Property(u => u.UserName).HasMaxLength(256);
b.Property(u => u.NormalizedUserName).HasMaxLength(256);
b.Property(u => u.Email).HasMaxLength(256);
b.Property(u => u.NormalizedEmail).HasMaxLength(256);
b.HasOne(x => x.UserProfile).WithOne(x => x.User).HasForeignKey<ApplicationUserProfilePicture>(x => x.Id);
b.HasMany<IdentityUserClaim<int>>().WithOne().HasForeignKey(uc => uc.UserId).IsRequired();
b.HasMany<IdentityUserLogin<int>>().WithOne().HasForeignKey(ul => ul.UserId).IsRequired();
b.HasMany<IdentityUserToken<int>>().WithOne().HasForeignKey(ut => ut.UserId).IsRequired();
b.HasMany<ApplicationUserRoleOrganization>().WithOne().HasForeignKey(ur => ur.UserId).IsRequired();
});
I can create and query other tables. Only AppUser table doesn't work in Oracle, it works fine using the SQL Server provider.
Tell me if more info about anything is needed.
Update
User class:
public class ApplicationUser : Microsoft.AspNetCore.Identity.IdentityUser<int>
{
public string FirstName { get; set; }
public string LastName { get; set; }
[StringLength(450)]
public string PositionName { get; set; }
[StringLength(450)]
public string NationalCode { get; set; }
[StringLength(450)]
public string PersonnelCode { get; set; }
public bool IsActive { get; set; } = true;
public bool IsExternal { get; set; } = false;
[Required]
public OrganizationTypeEnum OrganizationType { get; set; }
public string OrganizationName { get; set; }
public virtual ApplicationUserProfilePicture UserProfile { get; set; }
public virtual ICollection<ApplicationUserRoleOrganization> UserRoleOrganizations { get; set; }
}
UserRoleOrganization class:
public class ApplicationUserRoleOrganization : Microsoft.AspNetCore.Identity.IdentityUserRole<int>
{
public int Id { get; set; }
public override int RoleId { get => base.RoleId; set => base.RoleId = value; }
public override int UserId { get => base.UserId; set => base.UserId = value; }
public virtual int OrganizationId { get; set; }
public virtual ApplicationUser User { get; set; }
public virtual ApplicationRole Role { get; set; }
public virtual Organization Organization { get; set; }
}
The configuration of UserRoleOrganization:
builder.Entity<ApplicationUserRoleOrganization>(b =>
{
b.HasKey(r => r.Id);
b.HasIndex(t => t.OrganizationId).HasName("OrganizationIndex");
b.HasIndex(t => t.RoleId).HasName("RoleIndex");
b.HasIndex(t => t.UserId).HasName("UserIndex");
b.HasOne(userRole => userRole.Role)
.WithMany(role => role.UserRoleOrganizations)
.HasForeignKey(userRole => userRole.RoleId).HasConstraintName("FK_RoleOrg_Role_RoleId");
b.HasOne(userRole => userRole.Organization)
.WithMany(role => role.UserRoleOrganizations)
.HasForeignKey(userRole => userRole.OrganizationId).HasConstraintName("FK_RoleOrg_Org_OrgId");
b.HasOne(userRole => userRole.User)
.WithMany(user => user.UserRoleOrganizations)
.HasForeignKey(userRole => userRole.UserId).HasConstraintName("FK_RoleOrg_User_UserId");
b.ToTable("AppUserRoleOrganization");
});
I just noticed a text is written in my output just before the exception:
fail: Microsoft.EntityFrameworkCore.Query[10100]
An exception occurred while iterating over the results of a query for context type 'Base.Security.Data.SecurityDbContext'.
You have included UserRoleOrganizations twice . Try to do including the good old way:
string username = "Administrator";
return _context.Users.Where(x => x.UserName.ToUpper() == username.ToUpper())
.Include(x => x.UserProfile)
.Include("UserRoleOrganizations.Organization");
.Include("UserRoleOrganizations.Role")
.FirstOrDefault();
But I don't like UserRoleOrganizations.Organization . Could you please show your User and UserRoleOrganizations clases?
Update.
Since I can see the classes, I think you can try to reverse query, maybe just for testing:
var userRoleOrganizations=_context.UserRoleOrganizations
.Where(x => x.User.UserName.ToUpper() == username.ToUpper())
.Include(s => s.Role)
.Include(s => s.Organization)
.ToList();
var user = _context.Users
.Where(x => x.UserName.ToUpper() == username.ToUpper())
.Include(x => x.UserProfile)
.FirstOrDefault();
user.UserRoleOrganizations=userRoleOrganizations;
return user;
And try to change the UserRoleOrganizations class:
public class UserRoleOrganizations
{
....
public override int? RoleId { get => base.RoleId; set => base.RoleId = value; }
public override int? UserId { get => base.UserId; set => base.UserId = value; }
public virtual int? OrganizationId { get; set; }
....
}

How and where to use AddRange() method

I want to display related data from second table with each value in first table
i have tried this query
public ActionResult Index()
{
List<EmployeeAtt> empWithDate = new List<EmployeeAtt>();
var employeelist = _context.TblEmployee.ToList();
foreach (var employee in employeelist)
{
var employeeAtt = _context.AttendanceTable
.GroupBy(a => a.DateAndTime.Date)
.Select(g => new EmployeeAtt
{
Date = g.Key,
Emp_name = employee.EmployeeName,
InTime = g.Any(e => e.ScanType == "I") ? g.Where(e =>
e.ScanType == "I").Min(e =>
e.DateAndTime.ToShortTimeString())
.ToString() : "Absent",
OutTime = g.Any(e => e.ScanType == "O") ? g.Where(e =>
e.ScanType == "O").Max(e =>
e.DateAndTime.ToShortTimeString())
.ToString() : "Absent"
});
empWithDate.AddRange(employeeAtt);
}
return View(empWithDate);
}
Here is my attendance Table
AttendanceTable
Results
I want to display the shortest time with "I" Column value against each employee and last time with "O" Column value as out time. I think i am not using AddRange() at proper place. Where it should go then?
public partial class TblEmployee
{
public TblEmployee()
{
AttendanceTable = new HashSet<AttendanceTable>();
}
public int EmpId { get; set; }
public string EmployeeName { get; set; }
public virtual ICollection<AttendanceTable> AttendanceTable { get; set; }
}
public partial class AttendanceTable
{
public int Id { get; set; }
public int AttendanceId { get; set; }
public int EmployeeId { get; set; }
public string ScanType { get; set; }
public DateTime DateAndTime { get; set; }
public virtual TblEmployee Employee { get; set; }
}
The actual problem is not related to AddRange(), you need a where clause before GroupBy() to limit attendances (before grouping) to only records related to that specific employee, e.g.
_context.AttendanceTable
.Where(a => a.Employee == employee.EmployeeName)
.GroupBy(a => a.DateAndTime.Date)
...
Depended on your model, it is better to use some kind of ID instead of EmployeeName for comparison if possible.
Also you can use SelectMany() instead of for loop and AddRange() to combine the results into a single list. like this:
List<EmployeeAtt> empWithDate = _context.TblEmployee.ToList()
.SelectMany(employee =>
_context.AttendanceTable
.Where(a => a.Employee == employee.EmployeeName)
.GroupBy(a => a.DateAndTime.Date)
.Select(g => new EmployeeAtt
{
...
})
);
...

Get first object in child collection inside projection

I have the following entities:
public class Mark {
public Int32 Id { get; set; }
public DateTime Created { get; set; }
public virtual ICollection<MarkLocalized> MarksLocalized { get; set; }
} // Mark
public class MarkLocalized {
public Int32 Id { get; set; }
public String Culture { get; set; }
public String Name { get; set; }
public virtual Mark Mark { get; set; }
} // MarkLocalized
Given a culture, for example "en", I need to get all Marks, their localized name for the given culture and map them to a MarkModel:
public class MarkModel {
public Int32 Id { get; set; }
public String Name { get; set; }
} // MarkModel
I tried the following:
Context context = new Context();
context.Marks
.SelectMany(x => x.MarksLocalized, (y, z) =>
new MarkModel {
Id = y.Id,
Name = z.Name,
Slug = z.Slug
});
But I need only the MarksLocalized which culture is equal to the given culture.
How can I do this?
Thank you,
Miguel
You can add Where inside SelectMany:
Context context = new Context();
context.Marks
.SelectMany(x => x.MarksLocalized.Where(x => x.Culture == "en"), (y, z) =>
new MarkModel {
Id = y.Id,
Name = z.Name,
Slug = z.Slug
});
But should be more clear with syntax query:
from m in new Context.Marks
from l in m.MarksLocalized
where l.Culture == "en"
select new MarkModel { m.ID, m.Name, l.Slug }
which should be translated into following query by compiler:
context.Marks
.SelectMany(x => x.MarksLocalized, (m, l) => new { m, l })
.Where(x => x.l.Culture == "en")
.Select(x => new MarkModel { x.m.ID, x.m.Name, x.l.Slug })
which should produce exact same results as first one.

KendoUI Scheduler

I am trying to implement the scheduler control to show live calculation of material usage on daily bases (in a week by week view).
I am unable to have the Usage data displayed in the cells although I managed to have to Materials displayed on the left hand side. I wonder if any one could help or give me some hints on what I am doing wrong. Here is my code so far:
I can be sure that data is being return to the view but the view is not showing the usage data, which is simply numeric values corresponding to a material on a specific day of the week. I have also attached a screenshot of how it looks like:
the Controller method to read the data:
public JsonResult Read([DataSourceRequest] DataSourceRequest request)
{
try
{
var usageList = new List<ReportUsageViewModel>();
var imports = _importRespository.GetImports();
foreach (var usageReportStoredProcedure in imports)
{
var usageViewModel = new ReportUsageViewModel();
usageViewModel.MaterialID = usageReportStoredProcedure.MaterialID;
usageViewModel.Start = usageReportStoredProcedure.ScanDate;
usageViewModel.End = usageReportStoredProcedure.ScanDate;
usageViewModel.DailyUsage = usageReportStoredProcedure.UsageQuantity;
usageViewModel.Title = usageReportStoredProcedure.UsageQuantity.ToString();
usageList.Add(usageViewModel);
}
return Json(usageList.ToDataSourceResult(request));
}
catch (Exception exc)
{
ErrorHelper.WriteToEventLog(exc);
return null;
}
}
The actual control
<div id="StockViewer">
#(Html.Kendo().Scheduler<WorcesterMarble.ViewModels.ReportUsageViewModel>()
.Name("StockViewer")
.Timezone("Europe/London")
.Resources(resource => resource.Add(m => m.MaterialID)
.Title("Materials")
.Name("Materials")
.DataTextField("Name")
.DataValueField("MaterialID")
.BindTo(Model.MaertiaList))
.MajorTick(270)
.MinorTickCount(1)
.StartTime(DateTime.Now.Date.AddHours(8))
.EndTime(DateTime.Now.Date.AddHours(17))
.AllDaySlot(false)
.Date(DateTime.Now.Date)
.Editable(false)
.Views(x => x.WeekView(v =>
{
v.Footer(false);
v.Selected(true);
v.DateHeaderTemplate("<span class='k-link k-nav-day'>#=kendo.toString(date, 'ddd dd/M')#</span>");
}))
.Group(group => group.Resources("Materials").Orientation(SchedulerGroupOrientation.Vertical))
.DataSource(d => d
.Model(m => {
m.Id(f => f.MaterialID);
m.Field(f => f.Title).DefaultValue("No title");
})
.Read("Read", "ReportUsage")
)
)
Update: This is the ViewModel implementing the ISchedulerEvent
public class ReportUsageViewModel : ISchedulerEvent
{
public int MaterialID { get; set; }
public string MaterialName { get; set; }
public int? DailyUsage { get; set; }
public List<MaterialViewModel> MaertiaList { get; set; }
public string Description { get; set; }
public System.DateTime End { get; set; }
public bool IsAllDay { get; set; }
public string RecurrenceException { get; set; }
public string RecurrenceRule { get; set; }
public System.DateTime Start { get; set; }
public string Title { get; set; }
}
The issue was in these two lines:
.StartTime(DateTime.Now.Date.AddHours(8))
.EndTime(DateTime.Now.Date.AddHours(17))
The data was there but these two lines were hiding it. The data was being logged at times outside the time range of 8 to 17 so I removed these two lines and then set the Major ticks to 1440, which is the total number of ticks in 24 hrs, which helped me hiding the time column as I didn't need it..

Telerik MVC Grid with Dynamic Columns at Run Time from a Collection or Dictionary

After spending the last couple days searching, I'm officially stuck. I'm working on a binding an object to the Telerik MVC 3 Grid, but the catch is that it needs to have dynamically created columns (not auto generated). Three of the columns are known, the others are unknown, and this is the tricky part. Basically, it can be like these examples:
KnownColumn1 | KnownColumn2 | UnknownColumn1 | KnownColumn3
KnownColumn1 | KnownColumn2 | UnknownColumn1 | UnknownColumn2 | UnknownColumn3 | KnownColumn3
etc.
Because I'm putting the unknown columns in a list (I've tried a dictionary too so I can get the column names), this has complicated things for me when binding. My code is below:
Model (There can be zero to hundreds of rows, but this model is in a view model of type List, there can also be 0 to 20 plus columns that are dynamically added)
public class VendorPaymentsGLAccount
{
public string GeneralLedgerAccountNumber { get; set; }
public string GeneralLedgerAccountName { get; set; }
public string DisplayName { get { return string.Format("{0} - {1}", GeneralLedgerAccountNumber, GeneralLedgerAccountName); } }
public Dictionary<string, double> MonthAmount { get; set; }
public double Total { get { return MonthAmount.Sum(x => x.Value); } }
public List<string> Columns { get; set; }
public List<double> Amounts { get; set; }
public VendorPaymentsGLAccount() { }
}
View (The section that's commented out was trying to use the dictionary)
<fieldset>
<legend>General Ledger Account Spend History</legend>
#if (Model.VendorPaymentsGLAccounts != null)
{
<br />
#(Html.Telerik().Grid(Model.VendorPaymentsGLAccounts)
.Name("Grid")
.Columns(columns =>
{
columns.Bound(gl => gl.DisplayName).Title("General Ledger Account").Width(200).Filterable(false).Sortable(false);
//foreach (var month in Model.VendorPaymentsGLAccounts[0].MonthAmount)
//{
// //columns.Bound(gl => gl.MonthAmount[month.Key.ToString()].ToString()).Title(month.Key.ToString()).Width(100).Filterable(false).Sortable(false);
// //columns.Template(v => Html.ActionLink(v.VoucherID, "VoucherSummary", new { id = v.VoucherID, bu = v.BusinessUnitID, dtt = v.InvoiceDate.Ticks })).Title("Voucher").Width(100);
// columns.Template(gl => Html.ActionLink(gl.MonthAmount[month.Key.ToString()].ToString(), "VoucherSummary")).Title(month.Key.ToString()).Width(100);
//}
for (int i = 1; i <= (Model.VendorPaymentsGLAccounts[0].Columns.Count() - 1); i++)
{
string colTemp = Model.VendorPaymentsGLAccounts[0].Columns[i - 1];
columns.Template(gl => gl.Amounts[i - 1]).Title(colTemp).Width(100);
}
columns.Template(gl => String.Format("{0:C}", gl.Total)).Title("Total");
})
.Sortable()
.Pageable()
.Filterable()
.Footer(true))
}
else
{
<br />
#:There are no records that match your selected criteria.
}
</fieldset>
Using the dictionary approach, I was able to get the columns generated correctly with the right header text, but the values for the columns (in my testing there were only 2 columns) were the same. Can anyone help with this? This seems to be an oddball issue. Just trying to figure out how to do this correctly.
Update: Here is a screen shot using the dictionary approach that shows the issue. The column headings are correct, but the values are the same for both of the dynamic columns.
Using dynamically defined columns with the Telerik grid control can be tricky. But in your case, it's mainly a typical pitfall of closures.
In the following loop, the compiler will bind each instance of gl => gl.Amounts[i - 1] to the variable i and evaluate it later:
for (int i = 1; i <= (Model.VendorPaymentsGLAccounts[0].Columns.Count() - 1); i++)
{
string colTemp = Model.VendorPaymentsGLAccounts[0].Columns[i - 1];
columns.Template(gl => gl.Amounts[i - 1]).Title(colTemp).Width(100);
}
In fact, it's evaluated after the loop has finished. So i will always have the value that lead to the completion of the loop.
The fix is to use a temporary variable:
for (int i = 1; i <= (Model.VendorPaymentsGLAccounts[0].Columns.Count() - 1); i++)
{
string colTemp = Model.VendorPaymentsGLAccounts[0].Columns[i - 1];
int columnIndex = i - 1;
columns.Template(gl => gl.Amounts[columnIndex]).Title(colTemp).Width(100);
}
I had the same problem, and was googling plenty of hours around, and have done a lot of attempts from various assistance lines. But even so, it was not so trivial to solve!
And for that reason and to have also another working example here, I will provide also my solution!
Information: It only works at my place with an IList Model. Other collections had also caused problems!
#model IList<CBS.Web.Models.Equipment.EquipmentViewModel>
#(Html.Telerik().Grid(Model)
.Name("Grid")
.DataKeys(keys =>
{
keys.Add(m => m.ID);
})
.DataBinding(dataBinding =>
{
dataBinding.Ajax()
// renders the grid initially
.Select("EquipmentGrid", "Equipment");
})
.Columns(columns =>
{
// Equipment IDs
columns.Bound(m => m.ID).Hidden(true);
columns.Bound(m => m.Name).Title("Equipments").Width(200);
// Every item (EquipmentViewModel) of the Model has the same Count of Fields
for (int i = 0; i < (Model[0].Fields.Count()); i++)
{
// Name of the column is everytime same as in Model[0]
string columnName = Model[0].Fields.ElementAt(i).FieldDefinition.Name;
// Constructs i-counted columns, dynamically on how much
// Fields are owned by an Equipment. But note, that all Equipment-items
// in the Model must have the same Count and disposal of Fields!
columns.Template(m => m.Fields
.Where(f => f.FieldDefinition.Name == columnName)
.Where(f => f.EquipmentId == m.ID).First().Value)
.Title(columnName)
.Width(columnName.Length * 8); // * 8 was the optimal lenght per character
}
})
.ClientEvents(events => events.OnRowSelect("onRowSelected"))
.Selectable()
.Resizable(resizing => resizing.Columns(true))
.Pageable()
.Scrollable()
.Groupable()
.Filterable()
)
Controller:
public ActionResult EquipmentGrid(Guid id)
{
var belongingEquipments = _equipmentRepository.GetNotDeleted()
.OrderBy(e => e.Name).ToList()
.Where(e => e.RevisionId == id);
List<EquipmentViewModel> equVMList = new List<EquipmentViewModel>();
for (int i = 0; i < belongingEquipments.Count(); i++)
{
var equVM = new EquipmentViewModel
{
ID = belongingEquipments.ElementAt(i).ID,
Name = belongingEquipments.ElementAt(i).Name,
RevisionId = belongingEquipments.ElementAt(i).RevisionId,
EquipmentTypeId = belongingEquipments.ElementAt(i).EquipmentTypeId,
Fields = SortFields(belongingEquipments.ElementAt(i).Fields.ToList())
};
equVMList.Add(equVM);
}
return PartialView("EquipmentGrid", equVMList);
}
Models:
namespace CBS.Web.Models.Equipment
{
public class EquipmentViewModel
{
public Guid ID { get; set; }
public string Name { get; set; }
public Guid RevisionId { get; set; }
public Guid EquipmentTypeId { get; set; }
public virtual ICollection<FieldEntity> Fields { get; set; }
}
}
FieldDefinition
namespace CBS.DataAccess.Entities
{
public class FieldDefinitionEntity : EntityBase
{
[Required]
public virtual Guid EquipmentTypeId { get; set; }
public virtual EquipmentTypeEntity EquipmentType { get; set; }
[Required(AllowEmptyStrings = false)]
public virtual string Name { get; set; }
public virtual int Numbering { get; set; }
[Required]
public virtual Guid TypeInformationId { get; set; }
public virtual TypeInformationEntity TypeInformation { get; set; }
public virtual ICollection<FieldEntity> Fields { get; set; }
}
}
Field
namespace CBS.DataAccess.Entities
{
public class FieldEntity : EntityBase
{
[Required]
public virtual Guid EquipmentId { get; set; }
public virtual EquipmentEntity Equipment { get; set; }
[Required]
public virtual Guid FieldDefinitionId { get; set; }
public virtual FieldDefinitionEntity FieldDefinition { get; set; }
public virtual string Value { get; set; }
}
}
I dynamically bind the columns at runtime with reflection:
#model IEnumerable<object>
#using System.Collections
#using System.Collections.Generic
#using System.Reflection;
#(Html.Telerik().Grid(Model)
.Name("Grid")
.Columns(columns =>
{
Type t = Model.GetType().GetGenericArguments()[0];
foreach (var prop in t.GetProperties())
{
if (IsCoreType(prop.PropertyType))
{
columns.Bound(prop.PropertyType, prop.Name);
}
}
})
.DataBinding(binding => binding.Ajax()
.Select("SelectMethod", "SomeController")
)
.Sortable()
.Pageable()
.Filterable()
.Groupable()
)
#functions{
public bool IsCoreType(Type type)
{
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
return IsCoreType(type.GetGenericArguments()[0]);
}
return !(type != typeof(object) && Type.GetTypeCode(type) == TypeCode.Object);
}
}
Here is the workaround:
#(Html.Telerik().Grid(Model.Users)
.Name("Grid")
.Columns(columns => {
columns.GenerateCustomColumns(columnSettings);
}).DataBinding(dataBinding => dataBinding.Ajax().Select("_getusers", "home"))
.Scrollable(scrolling => scrolling.Enabled(true).Height("auto"))
.Pageable(paging => paging.Enabled(true)
.PageSize(10, new int[] { 5, 10, 20, 50, 100, 500 })
.Position(GridPagerPosition.Both)
.Total(Model.Users.Count)
.Style(GridPagerStyles.PageSizeDropDown | GridPagerStyles.NextPreviousAndNumeric)
.PageTo(1))
.Filterable(filtering => filtering.Enabled(true))
.Reorderable(reordering => reordering.Columns(true))
.NoRecordsTemplate(" ")
.EnableCustomBinding(true)
)
// Extension method to genarate columns dynamically
public static class TelerikMvcGridColumnHelper
{
public static void GenerateCustomColumns<T>(this GridColumnFactory<T> columns,List<GridCustomColumnSettings> settings) where T:class
{
if (settings != null)
{
settings.ForEach(column =>
{
var boundedColumn = columns.Bound(column.Member);
if (column.ClientFooterTemplate != null)
boundedColumn.ClientFooterTemplate(column.ClientFooterTemplate);
if (!string.IsNullOrEmpty(column.Width))
boundedColumn.Width(column.Width);
});
}
}
}
// Column settings class
public class GridCustomColumnSettings : GridColumnSettings
{
public string ClientFooterTemplate { get; set; }
}
I did this simple way. NOTE : the following solution works in ajax edit mode too (not just a read-only grid) :
when the ViewModels are :
public class PriceSheetEditGridViewModel
{
public IEnumerable<PriceSheetRowViewModel> Rows { get; set; }
public IEnumerable<PriceSheetColumnViewModel> Columns { get; set; }
}
public class PriceSheetColumnViewModel
{
public int Id { get; set; }
public string Title { get; set; }
}
public class PriceSheetRowViewModel
{
public int RowNo { get; set; }
public string Description { get; set; }
public double?[] Prices { get; set; }
}
the view can be like this (part of view.cshtml file...) :
....
#model PriceSheetEditGridViewModel
...
columns.Bound(o => o.Description ).Width(150);
int i = 0;
foreach (var col in Model.Columns)
{
columns
.Bound(model => model.Prices).EditorTemplateName("PriceSheetCellPrice").EditorViewData(new { ColumnId = i })
.ClientTemplate("<span><#=Prices ? jsHelper.addCommas(Prices[" + i.ToString() + "]):null#></span>")
.Title(col.Title).Width(80);
i++;
}
....
and the PriceSheetCellPrice.cshtml editor template file (in shared\editortemplates folder) :
#model decimal?
#(Html.Telerik().NumericTextBox()
.Name(ViewData.TemplateInfo.GetFullHtmlFieldName(string.Empty)+"["+ViewBag.ColumnId+"]")
.InputHtmlAttributes(new { style = "width:100%" })
})
.EmptyMessage("")
.DecimalDigits(0)
.DecimalSeparator(",")
.MinValue(0)
.Value((double?) Model)
)

Resources