Serilog: Usage of additional properties - serilog

I'm trying to write additional properties to the log using {Properties} (which are not in the message template):
Used (FileSink) template:
"[{Level}] {Message}{NewLine}{Properties}{NewLine}{Exception}"
Log operation (simplified, normally the object array is given by a method parameter):
Log.Information("Start logging",
new object[]{
new { Version = "VersionString"},
new { StartDate = DateTime.Now },
new { Id = Guid.NewGuid() }
});
I also tired:
Log.Information("Start logging",
new object[]{
"VersionString",
DateTime.Now,
Guid.NewGuid()
});
I had a look on LogEventPropertyCapturingTests and this PR, but I couldn't get it working...
Update
I use wrapper functions like this:
public static void Information(string messageTemplate, object[] propertyValues, bool show = false, [CallerMemberName] string callerMethodeName = "", [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLineNumer = -1)
{
using (LogContext.PushProperty("CallingContext", new { callerMethodeName, callerFilePath, callerLineNumer }))
{
_MainLog.Information(messageTemplate, propertyValues);
}
if(show)
{
// Code to show a the event to the user
}
}
Update2 Found a way but it's not very nice, as the template-property-matching rudimentary.
public static void Information(string messageTemplate, object[] propertyValues, bool show = false, [CallerMemberName] string callerMethodeName = "", [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLineNumer = -1)
{
using (LogContext.PushProperty("CallingContext", new { callerMethodeName, callerFilePath, callerLineNumer }))
{
Regex matchProperties = new Regex("{[^}]+}");
int usedPropertiesCount = matchProperties.Matches(messageTemplate).Cast<Match>().Select(m => m.Value).Distinct().Count();
if (propertyValues.Length > usedPropertiesCount)
{
using (LogContext.PushProperty("AdditionalData", propertyValues.Skip(usedPropertiesCount)))
{
_MainLog.Information(messageTemplate, propertyValues);
}
}
else
{
_MainLog.Information(messageTemplate, propertyValues);
}
}
if(show)
{
// Code to show a the event to the user
}
}

The ForContext() method will do this:
Log.ForContext("Version", "VersionString")
.ForContext("Id", Guid.NewGuid())
.Information("Start logging");
(I've omitted StartDate because all Serilog events are already timestamped.)
This blog post series includes some posts on message templates plus context and correlation that cover this plus other alternatives.

Related

Blazor Webassembly: How to insert components into a string

I have this component that displays generic messages:
<span>#message</span>
The messages are identified by an id and come from string tables in resources files (multiple languages). An example of a message would be:
"Hello {user}! Welcome to {site}!"
So in the basic case, I simply parse the string and replace {user} with, say, "John Doe" and {site} with "MySiteName". The result is set to message and is then properly (and safely) rendered.
But what I would like to do is actually replace {site} with a component that I created that displays the site name with special font and styling. I also have other cases where I want to replace special {markings} with components.
How would you approach this problem ? Is there a way to "insert" a component into a string and then insert the string "safely" to be rendered ? I say "safely" because portions of the final string may come from the DB and be inherently unsafe (like user's name) so inserting the string with something like #((MarkupString)message) does not seem safe.
EDIT:
Thanks to MrC aka Shaun Curtis from whom this final solution is greatly inspired. I marked his answer as the best one.
So I finally went with a scoped service that gets the strings from the resources files, parse them and return a list of RenderFragments that it gets from a component's static table. I use dynamic objects to send specific parameters to the RenderFragments when required.
I basically now get all the text of my app through this centralized mechanism.
Here is an example of an entry in a resource file string table:
Name: "welcome"; Value: "Welcome to {site:name} {0}!"
Here is how it is used in a component:
<h3><Localizer Key="notif:welcome" Data="#(new List<string>() { NotifModel.UserNames.First })"/></h3>
You can see the simplified component and service code below. I explicitely left out the validation and error checking code for simplicity.
#using MySite.Client.Services.Localizer
#inject ILocalizerService Loc
#foreach (var fragment in _fragments)
{
#fragment.Renderer(fragment.Item)
}
#code
{
private List<ILocalizerService.Fragment> _fragments;
public enum RendererTypes
{
Default,
SiteName,
SiteLink,
}
public static Dictionary<RendererTypes, RenderFragment<dynamic>> Renderers = new Dictionary<RendererTypes, RenderFragment<dynamic>>()
{
// NOTE: For each of the following items, do NOT insert a space between the end of the markup and the closing curly brace otherwise it will be rendered !!!
// Like here ↓↓
{ RendererTypes.Default, (model) => #<span>#(model as string)</span>},
{ RendererTypes.SiteName, (model) => #<MySiteNameComponent />},
{ RendererTypes.SiteLink, (model) => ##model.LinkTxt}
};
[Parameter]
public string Key { get; set; }
[Parameter]
public List<string> Data { get; set; }
protected override void OnParametersSet()
{
_fragments = Loc.GetFragments(Key, Data);
}
}
interface ILocalizerService
{
public struct Fragment
{
public Fragment(RenderFragment<dynamic> renderer)
: this(renderer, default)
{
}
public Fragment(RenderFragment<dynamic> renderer, dynamic item)
{
Renderer = renderer;
Item = item;
}
public RenderFragment<dynamic> Renderer { get; set; }
public dynamic Item { get; set; }
}
List<Fragment> GetFragments(string key, List<string> parameters);
}
internal sealed class LocalizerService : ILocalizerService
{
private readonly Dictionary<string, IStringLocalizer> _strLoc = new Dictionary<string, IStringLocalizer>();
public LocalizerService(IStringLocalizer<MySite.Shared.Resources.App> appLoc,
IStringLocalizer<MySite.Shared.Resources.Connection> connLoc,
IStringLocalizer<MySite.Shared.Resources.Notifications> notifLoc)
{
// Keep string localizers
_strLoc.Add("app", appLoc);
_strLoc.Add("conn", connLoc);
_strLoc.Add("notif", notifLoc);
}
public List<Fragment> GetFragments(string key, List<string> parameters)
{
var list = new List<Fragment>();
GetFragments(list, key, parameters);
return list;
}
private void GetFragments(List<Fragment> list, string key, List<string> parameters)
{
// First, get key tokens
var tokens = key.Split(':');
// Analyze first token
switch (tokens[0])
{
case "site":
// Format : {site:...}
ProcessSite(list, tokens, parameters);
break;
default:
// Format : {0|1|2|...}
if (uint.TryParse(tokens[0], out var paramIndex))
{
ProcessParam(list, paramIndex, parameters);
}
// Format : {app|conn|notif|...}
else if (_strLoc.ContainsKey(tokens[0]))
{
ProcessStringLocalizer(list, tokens, parameters);
}
break;
}
}
private void ProcessSite(List<Fragment> list, string[] tokens, List<string> parameters)
{
// Analyze second token
switch (tokens[1])
{
case "name":
// Format {site:name}
// Add name component
list.Add(new Fragment(Shared.Localizer.Renderers[Shared.Localizer.RendererTypes.SiteName]));
break;
case "link":
// Format {site:link:...}
ProcessLink(list, tokens, parameters);
break;
}
}
private void ProcessLink(List<Fragment> list, string[] tokens, List<string> parameters)
{
// Analyze third token
switch (tokens[2])
{
case "user":
// Format: {site:link:user:...}
ProcessLinkUser(list, tokens, parameters);
break;
}
}
private void ProcessLinkUser(List<Fragment> list, string[] tokens, List<string> parameters)
{
// Check length
var length = tokens.Length;
if (length >= 4)
{
string linkUrl;
string linkTxt;
// URL
// Format: {site:link:user:0|1|2|...}
// Retrieve handle from param
if (!uint.TryParse(tokens[3], out var paramIndex))
{
throw new ApplicationException("Invalid token!");
}
var userHandle = GetParam(paramIndex, parameters);
linkUrl = $"/user/{userHandle}";
// Text
if (length >= 5)
{
if (tokens[4].Equals("t"))
{
// Format: {site:link:user:0|1|2|...:t}
// Use token directly as text
linkTxt = tokens[4];
}
else if (uint.TryParse(tokens[4], out paramIndex))
{
// Format: {site:link:user:0|1|2|...:0|1|2|...}
// Use specified param as text
linkTxt = GetParam(paramIndex, parameters);
}
}
else
{
// Format: {site:link:user:0|1|2|...}
// Use handle as text
linkTxt = userHandle;
}
// Add link component
list.Add(new Fragment(Shared.Localizer.Renderers[Shared.Localizer.RendererTypes.SiteLink], new { LinkUrl = linkUrl, LinkTxt = linkTxt }));
}
}
private void ProcessParam(List<Fragment> list, uint paramIndex, List<string> parameters)
{
// Add text component
list.Add(new Fragment(Shared.Localizer.Renderers[Shared.Localizer.RendererTypes.Default], GetParam(paramIndex, parameters)));
}
private string GetParam(uint paramIndex, List<string> parameters)
{
// Proceed
if (paramIndex < parameters.Length)
{
return parameters[paramIndex];
}
}
private void ProcessStringLocalizer(List<Fragment> list, string[] tokens, List<string> parameters)
{
// Format {loc:str}
// Retrieve string localizer
var strLoc = _strLoc[tokens[0]];
// Retrieve string
var str = strLoc[tokens[1]].Value;
// Split the string in parts to see if it needs formatting
// NOTE: str is in the form "...xxx {key0} yyy {key1} zzz...".
// This means that once split, the keys are always at odd indexes (even if {key} starts or ends the string)
var strParts = str.Split('{', '}');
for (int i = 0; i < strParts.Length; i += 2)
{
// Get parts
var evenPart = strParts[i];
var oddPart = ((i + 1) < strParts.Length) ? strParts[i + 1] : null;
// Even parts are always regular text. If not null or empty, we add directly
if (!string.IsNullOrEmpty(evenPart))
{
list.Add(new Fragment(Shared.Localizer.Renderers[Shared.Localizer.RendererTypes.Default], evenPart));
}
// Odd parts are always keys. If not null or empty, get fragments recursively
if (!string.IsNullOrEmpty(oddPart))
{
GetFragments(list, oddPart, parameters);
}
}
}
}
You don't necessarily need to build components. A component is a c# class that emits a RenderFragment.
You could simply build RenderFragments for {site},... Here's a simple static class that shows two ways to do this:
namespace StackOverflowAnswers;
public static class RenderFragements
{
public static RenderFragment SiteName => (builder) =>
{
// Get the content from a service that's accessing a database and checking the culture info for language
builder.OpenElement(0, "div");
builder.AddAttribute(1, "class", "p-2 bg-primary text-white");
builder.AddContent(2, "My Site");
builder.CloseElement();
};
public static RenderFragment GetSiteName(string sitename) => (builder) =>
{
// parse to make sure you're happy with the string
builder.OpenElement(0, "span");
builder.AddAttribute(1, "class", "p-2 bg-dark text-white");
builder.AddContent(2, sitename);
builder.CloseElement();
};
}
And here's an index page using them:
#page "/"
#using StackOverflowAnswers
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<SurveyPrompt Title="How is Blazor working for you?" />
<div class=m-2>
The site name for this site is #(RenderFragements.GetSiteName("this site"))
</div>
#(RenderFragements.SiteName)
With the RenderFragment your writing c# code. You can run a parser to check the string before rendering it.
You could have a scoped service that gets the info from the database for the user and exposes a set of RenderFragments you then use in your pages/components.
I used regex to split the source at the tokens configured in TokenMappings. Token mappings could easily be loaded from a json source for example. To configure more "{markings}" just add more lines to the TokenMappings.
<StringParser Source="Hello {user}! Welcome to {site}!" />
StringParser.razor
#foreach (var subString in substrings)
{
if (tokens.Contains(subString))
{
var key = StripCurlyBrackets(subString);
<DynamicComponent Type=#(TokenMappings[key].Item1)
Parameters=#(TokenMappings[key].Item2) />
}
else
{
#subString
}
}
#code {
private Dictionary<string, (Type, Dictionary<string, object>?)> TokenMappings;
private string[] substrings;
private string[] tokens;
[Parameter]
public string Source { get; set; }
protected override void OnParametersSet()
{
var user = "John Doe"; // I would expect these are supplied via a signin context.
var site = "MySiteName"; //
TokenMappings = new Dictionary<string, (Type, Dictionary<string, object>?)>
{
{ "user", ( typeof(UserComponent), new Dictionary<string, object>{ { "User", user } } ) },
{ "site", ( typeof(SiteComponent), new Dictionary<string, object>{ { "Site", site } } ) }
};
var keys = TokenMappings.Keys.Select(a => a);
var pattern = keys.Select(key => $"({{(?:{key})}})").Aggregate((a, b) => a + "|" + b);
this.substrings = System.Text.RegularExpressions.Regex.Split(Source, pattern);
this.tokens = TokenMappings!.Keys.Select(key => $"{{{key}}}").ToArray();
base.OnParametersSet();
}
private string StripCurlyBrackets(string source)
{
return source
.Replace(oldValue: "{", newValue: string.Empty)
.Replace(oldValue: "}", newValue: string.Empty);
}
}
Yes MarkupString allows you to render html.
substrings :

Using Blazor with Electron.NET, how can I execute code inside a component whenever I click on a MenuItem?

I am using Blazor Server.
Inside my startup I am configuring the MenuItems like this:
var menu = new[]
{
new MenuItem
{
Label = "File", Submenu = new[]
{
new MenuItem
{
Label = "Save",
Accelerator = "CmdOrCtrl+S",
Enabled = false,
Click = () =>
{
// How do I execute code inside my component?
},
}
}
}
};
Electron.Menu.SetApplicationMenu(menu);
My component lives on the root and is always rendered. It has a simple method called Save which I want to call.
public async Task SaveProject()
{
await Project.Save();
}
Isn't there some kind of event inside the static Electron class that I could use? Something like OnMenuItemClicked.
Having a static property that I could access inside my component would not only be bad design, it would prevent me from accessing any instance properties.
I came up with a more or less applicable solution myself.
I have created a singleton service IMenuItemService which I am using in both my Startup and my Component. Since MenuItems do not have an ID per se, I created a seperate Enum MenuItemType to seperate them. The service looks something like this:
public class MenuItemService : IMenuItemService
{
public Action<MenuItemType> MenuItemClicked { get; set; }
private Dictionary<MenuItemType, MenuItem> ConfigurableMenuItems { get; }
public MenuItemService()
{
ConfigurableMenuItems = new Dictionary<MenuItemType, MenuItem>();
InitializeMenuItem(MenuItemType.Close, "Close Project", null, false);
}
private void InitializeMenuItem(MenuItemType type, string label, string accelerator, bool enabled)
{
ConfigurableMenuItems.Add(type, new MenuItem
{
Label = label,
Accelerator = accelerator,
Enabled = enabled,
Click = () => { MenuItemClicked?.Invoke(type); },
});
}
public void SetEnabled(MenuItemType menuItemType)
{
ConfigurableMenuItems[menuItemType].Enabled = true;
RenderMenuItems();
}
public void SetDisabled(MenuItemType menuItemType)
{
ConfigurableMenuItems[menuItemType].Enabled = false;
RenderMenuItems();
}
public void RenderMenuItems()
{
Electron.Menu.SetApplicationMenu(new[]
{
new MenuItem
{
Label = "File", Submenu = new []
{
ConfigurableMenuItems[MenuItemType.Close]
}
}
});
}
}
With this approach I can call menuItemService.RenderMenuItems() from anywhere in my application including Startup.cs while in my Components I am setting the MenuItemClicked Action in order to listen to clicks.
[Inject]
public IMenuItemService MenuItemService { get; set; }
private void InitializeMenuItemActions()
{
MenuItemService.SetEnabled(MenuItemType.Close);
MenuItemService.MenuItemClicked = type =>
{
if (type == MenuItemType.Close)
{
ProjectManager.CloseProject();
NavigationManager.NavigateTo("/");
}
};
}
In my case I intentionally used an Action Property instead on an EventHandler since I do not need multiple listeners for my MenuItem.

MVC SignalR not firing from Controller Post Method

When Saving schedule to calendar it must auto update the activity logs on my notification bar in my Home Controller. It saves the data but only show when notification bar is refreshed. It seems that Hub is not starting when saved.
CalendarController.cs
[HttpPost]
public JsonResult SaveSchedule(Schedule s)
{
var userid = User.Identity.GetUserId();
var profile = _context.Profiles.Single(p => p.Id == userid);
var status = false;
if (s.Schedule_ID > 0)
{
//Update
var v = _context.Schedules.Where(a => a.Schedule_ID == s.Schedule_ID).FirstOrDefault();
if (v != null)
{
v.Shift = s.Shift;
}
}
var activitylog = new ActivityLog
{
UserId = userid,
LogDate = DateTime.Now,
Activity = ActivityHelper.GetActivityLog(4, profile.FirstName)
};
// save to data and must be shown on notification bar
_context.ActivityLogs.Add(activitylog);
_context.SaveChanges();
ActivityHub.StartLogging();
status = true;
return new JsonResult { Data = new { status = status } };
}
HomeController.cs
public JsonResult GetLogs()
{
return Json(ActivityHelper.GetActivityLogs(), JsonRequestBehavior.AllowGet);
}
ActivityHub.cs
public class ActivityHub : Hub
{
public static void StartLogging()
{
IHubContext context = GlobalHost.ConnectionManager.GetHubContext<ActivityHub>();
//calls the signalR client part to execute javascript method
context.Clients.All.displayLog();
}
}
My CSHTML
<script>
$(function () {
var activityFromHub = $.connection.activityHub;
$.connection.hub.start().done(function () {
FetchLogs();
});
activityFromHub.client.displayLog = function () {
console.log('Hub Started');
FetchLogs();
}
function FetchLogs() {
$.ajax({
type: 'GET',
url: '/Home/GetLogs',
datatype: 'json',
success: function (data) {
$("#logs tr").remove();
data = $.parseJSON(data);
if (data.length > 0) {
.... do some append here
}
},
error: function (error) {
alert("error");
}
});
}
});
</script>
ActivityHelper.cs
static readonly string connString = ConfigurationManager.ConnectionStrings["ConnectionString"].ConnectionString;
public static class ActivityHelper
{
public static string GetActivityLogs()
{
string sqlCommand = #"my select query here";
try
{
var messages = new List<ActivityLog>();
using(var connection = new SqlConnection(connString))
{
connection.Open();
using (SqlConnection con = new SqlConnection(connString))
{
SqlCommand cmd = new SqlCommand(sqlCommand, con);
if(con.State != System.Data.ConnectionState.Open)
{
con.Open();
}
cmd.Notification = null;
SqlDependency dependency = new SqlDependency(cmd);
dependency.OnChange += new OnChangeEventHandler(dependency_OnChange);
var reader = cmd.ExecuteReader();
while (reader.Read())
{
messages.Add(item: new ActivityLog
{
Activity = reader["Activity"] != DBNull.Value ? (string)reader["Activity"] : "",
LogDate = (DateTime)reader["LogDate"]
});
}
}
}
var jsonSerialiser = new JavaScriptSerializer();
var json = jsonSerialiser.Serialize(messages);
return json;
}
catch(Exception ex)
{
throw;
}
}
public static void dependency_OnChange(object sender, SqlNotificationEventArgs e)
{
if (e.Type == SqlNotificationType.Change)
{
SqlDependency dependency = sender as SqlDependency;
dependency.OnChange -= dependency_OnChange;
var activityHub = GlobalHost.ConnectionManager.GetHubContext<ActivityHub>();
GetActivityLogs();
}
}
}
FIRST METHOD
First Solution change your javascript code like this. If this not works move to the second method:
$(function () {
var activityFromHub = $.connection.ActivityHub;
$.connection.hub.start().done(function () {
FetchLogs();
});
activityFromHub.client.displayLog = function () {
console.log('Hub Started');
FetchLogs();
}
});
SECOND METHOD:
Each client connecting to a hub passes a unique connection id. You can retrieve this value in the Context.ConnectionId property of the hub context. And i found there is nothing happening like this. You may try this solution.
I think the simplest solution for your question is to use groups.
http://www.asp.net/signalr/overview/guide-to-the-api/working-with-groups
Your hub class would contain methods to join a group:
public Task JoinGroup(string groupName)
{
return Groups.Add(Context.ConnectionId, groupName);
}
public Task LeaveGroup(string groupName)
{
return Groups.Remove(Context.ConnectionId, groupName);
}
and your hub will be look like this:
public static void StartLogging(string groupName)
{
IHubContext context = GlobalHost.ConnectionManager.GetHubContext<ActivityHub>();
context.Clients.Group(groupName).displayLog();
//calls the signalR client part to execute javascript method
//context.Clients.All.displayLog();
}
And change your javascript as like this:
$(function () {
var activityFromHub = $.connection.ActivityHub;
$.connection.hub.start().done(function () {
activityFromHub.server.joinGroup("Group1");
activityFromHub.server.StartLogging("Group1");
FetchLogs();
});
activityFromHub.client.displayLog = function () {
console.log('Hub Started');
FetchLogs();
}
});
I hope this will resolve your issue. If you are still facing issue. Please leave comments. Thank you.

Show highcharts for database data

I have tried to display a chart for database data but I'm having some trouble. I've tried using json to high charts but it's not working. Could someone please explain to me how to show axis as date and to call this on viewpage?
public List<CustomerAccount> ChartData(long customerID)
{
List<CustomerAccount> chart = new List<CustomerAccount>();
List<CustomerAccount> points = new CustomerAccountDBMgr().ChartAccount(customerID).FindAll(e => e.AccountName != "Others");
if (null == points || points.Count == 0)
return null;
var val = (from item in points select new { CreatedAt = item.CreatedAt, OpeningBalance = item.OpeningBalance }).ToList();
foreach (var data in val)
{
CustomerAccount objCustomer = new CustomerAccount()
{
CreatedAt = data.CreatedAt,
OpeningBalance = data.OpeningBalance
};
chart.Add(objCustomer);
}
return chart;
}
Model
public class Chart1
{
public DateTime CreatedAt { get; set; }
public Double? OpeningBalance { get; set; }
public List<CustomerAccount> ChartData()
{
PersonalizeBL business=new PersonalizeBL();
var CustomerID = PERFICSSession.GetCustomerID();
List<CustomerAccount>point=business.ChartData(CustomerID);
return point;
}
}
Controller
public ActionResult Chart1()
{
if (!PERFICSSession.IsValidSession())
return View("Login");
Chart1 model = new Chart1();
Highcharts chart = new Highcharts("chart")
.InitChart(new Chart { DefaultSeriesType = ChartTypes.Line })
.SetTitle(new Title { Text = "Database Data" })
.SetXAxis(new XAxis { Type = AxisTypes.Datetime })
.SetYAxis(new[]
{
new YAxis
{
Title = new YAxisTitle { Text = "Amount" },
GridLineWidth = 1
}
})
.SetSeries(new[]
{
new Series
{
Name = "OpeningBalance",
Data = new Data((model.ChartData()).Select(x=>new points{X=DotNet.Highcharts.Helpers.Tools.GetTotalMilliseconds(x.CreatedAt),Y=x.OpeningBalance}).ToArray())
}
});
return View("Chart1");
}
Finally i found answere for this issue. i would have return view which object i did created from highcharts. i wrongly returned the view page name.
public ActionResult Chart1()
{
chartModel model = new chartModel();
var data = model.chartPlots(Session);
Highcharts chart = new Highcharts("chart")
.InitChart(new Chart { DefaultSeriesType = ChartTypes.Line })
.SetTitle(new Title { Text = "Database Data" })
.SetXAxis(new XAxis { Type = AxisTypes.Datetime })
.SetYAxis(new[]
{
new YAxis
{
Title = new YAxisTitle { Text = "Amount" },
GridLineWidth = 1
}
})
.SetSeries(new[]
{
new Series
{
Name = "OpeningBalance",
Data = new Data(data.Select(x=>new points{X=DotNet.Highcharts.Helpers.Tools.GetTotalMilliseconds(x.CreatedAt),Y=x.OpeningBalance}).ToArray())
}
});
return View(chart);
}
return View(chart);
}

Cannot use ComboBox SelectedItem as BindingSource for cascaded ComboBox

I have 2 ComboBoxes on my form. I create the bindings as follows:
TestClass myclass = new TestClass("Instruments");
myclass.Add(instr1 = new TestClass("INSTR1"));
myclass.Add(instr2 = new TestClass("INSTR2"));
myclass.Add(instr3 = new TestClass("INSTR3"));
myclass.Add(instr4 = new TestClass("INSTR4"));
instr1.Add(app1 = new TestClass("app1"));
instr1.Add(app2 = new TestClass("app2"));
instr1.Add(app3 = new TestClass("app3"));
instr1.Add(app4 = new TestClass("app4"));
instr2.Add(app5 = new TestClass("app5"));
instr2.Add(app6 = new TestClass("app6"));
instr2.Add(app7 = new TestClass("app7"));
instr2.Add(app8 = new TestClass("app8"));
mysource = new BindingSource(myclass, null);
selectedComboBox1.DataSource = mysource;
selectedComboBox1.DisplayMember = "NAME";
mysource2 = new BindingSource(selectedComboBox1, "SelectedItem");
selectedComboBox2.DataSource = mysource2;
selectedComboBox2.DisplayMember = "NAME";
The class used for the binding looks as follows
class TestClass : BindingList<TestClass>, INotifyPropertyChanged
{
public event RunTestChanged RunTestChangedEventHandler;
public TestClass()
{
this.test = "";
this.name = "";
this.runTest = true;
}
public TestClass(string name)
{
this.test = "";
this.name = name;
this.runTest = true;
}
public TestClass LIST
{
get
{
return this;
}
}
public string NAME
{
get
{
return this.name;
}
set
{
this.name = value;
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs("NAME"));
}
}
}
public string TEST
{
get
{
return this.test;
}
set
{
this.test = value;
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs("TEST"));
}
}
}
public bool RUNTEST
{
get
{
return runTest;
}
set
{
runTest = value;
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs("RUNTEST"));
}
RunTestArgs myargs = new RunTestArgs(value);
if (RunTestChangedEventHandler != null)
{
RunTestChangedEventHandler(this, myargs);
}
}
}
private bool runTest;
private string name;
private string test;
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
}
when the form first loads the 2 comboboxes are filled as they should be with the expected items. However, if i change an item in selectedComboBox1, the items in selectedComboBox2 aren't updated. I know that I can subscribe to the selectedComboBox1 SelectedIndexChanged event and then rebind the DataSource on selectedComboBox2 and everything will work as expected.
For example:
void selectedComboBox1_SelectedIndexChanged(object sender, EventArgs e)
{
mysource2.DataSource = selectedComboBox1.SelectedItem;
mysource2.DataMember = null;
}
Another alternative that works is to perform the databinding as follows:
mysource = new BindingSource(myclass, null);
mysource2 = new BindingSource(mysource, "LIST");
mysource3 = new BindingSource(mysource2, "LIST");
selectedComboBox1.DataSource = mysource;
selectedComboBox1.DisplayMember = "NAME";
selectedComboBox2.DataSource = mysource2;
selectedComboBox2.DisplayMember = "NAME";
However I wanted to know if there was a way to avoid having to subscribe to the event or performing the databinding in a different manner and just have the 2nd ComboBox be updated via the BindingSource using the SelectedItem property. In the end I'm curious to know how to get the BindingSource to be updated via the SelectedItem databinding and if it's not possible what is preventing it from working.
Thank you for your help.
i have the same issue and got resolved by binding Name to SelectedValue of combobox and set ValueMember to be "NAME" property
selectedComboBox1.DisplayMember = "NAME";
selectedComboBox1.ValueMember = "NAME";

Resources