Notify users using SqlDependency - c#

I'm trying to notify multiple application users connected to the same SQLServer that change happened on certain data base table. I decided to use SQLDependency class because it seemed like the best way to do so, but I can't get it to work. Your help is very much appreciated.
First of all I created SqlWatcher class to simplify the usage (credits go to this blog):
public class SqlWatcher : IDisposable
{
public delegate void SqlWatcherEventHandler(DataSet Result);
public event SqlWatcherEventHandler OnChange;
private string ConnectionString;
private SqlConnection Connection;
private SqlCommand Command;
private SqlDataAdapter Adapter;
private DataSet Result;
public SqlWatcher(string ConnectionString, SqlCommand Command)
{
this.ConnectionString = ConnectionString;
SqlDependency.Start(this.ConnectionString);
this.Connection = new SqlConnection(this.ConnectionString);
this.Connection.Open();
this.Command = Command;
this.Command.Connection = this.Connection;
Adapter = new SqlDataAdapter(this.Command);
}
public void ChangeEventWrapper(object state)
{
DataSet Result = (DataSet)state;
OnChange(Result);
}
public void Dispose()
{
Stop();
}
private void RegisterForChanges()
{
//Remove old dependency object
this.Command.Notification = null;
//Create new dependency object
SqlDependency dep = new SqlDependency(this.Command);
dep.OnChange += new OnChangeEventHandler(Handle_OnChange);
//Save data
Result = new DataSet();
Adapter.Fill(Result);
OnChange(Result);
}
public void Start()
{
RegisterForChanges();
}
public void Stop()
{
SqlDependency.Stop(this.ConnectionString);
}
private void Handle_OnChange(object sender, SqlNotificationEventArgs e)
{
SqlDependency dep = (SqlDependency)sender;
dep.OnChange -= Handle_OnChange;
RegisterForChanges();
}
public DataSet DataSet
{
get { return Result; }
}
}
From the main form user can add an item to the targeted SqlDependency command table and when that happens I want new form to pop-up with news. This is what my main form looks like:
public partial class MainForm: Form
{
private string connectionString = #"data source = .\sqlexpress; initial catalog = Sample1; integrated security = true";
private static SqlWatcher SqlQueueWatcher;
public MainForm()
{
InitializeComponent();
InitializeSqlDependency();
}
private void InitializeSqlDependency()
{
SqlCommand cmd = new SqlCommand();
cmd = new SqlCommand("SELECT ID, Text FROM dbo.Notifications");
cmd.CommandType = CommandType.Text;
//Setup the SQLWatcher
SqlQueueWatcher = new SqlWatcher(connectionString, cmd);
SqlQueueWatcher.OnChange += new SqlWatcher.SqlWatcherEventHandler(QueueSQLWatcher_OnChange);
SqlQueueWatcher.Start();
}
private static void QueueSQLWatcher_OnChange(DataSet Result)
{
NotificationForm notificationForm = new NotificationForm(Result);
notificationForm.Show();
}
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
SqlQueueWatcher.Dispose();
}
I expected this to work the way that when any user adds data to the Notifications data base other users get notified via NotificationForm. Could anyone tell me what am I doing wrong? I've been trying to figure it out myself for 2 days now.
Thanks in advance!
P.S. If you have better idea on doing the same notification concept, feel free to express yourself.

Related

How to use textbox textChanged property with background worker to fill datagridview

in my form i have a textbox, background worker and a data grid view. using the textbox(TextChanged property) i want to fatch data using the background worker and fill matching data in the data grid view as i type.
here is what i have tried out.
private void txtSearchBox_TextChanged(object sender, EventArgs e)
{
if (backgroundWorker1.IsBusy)
{
backgroundWorker1.CancelAsync();
}
backgroundWorker1.RunWorkerAsync();///Runs the background worker
}
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
try
{
con.Open();
MySqlDataAdapter da = new MySqlDataAdapter("select * from Games where Name like '%" + txtSearchBox.Text + "%'", con);
da.Fill(ds, "GameID");
con.Close();
}
catch (Exception ex)
{
MessageBox.Show("Error" + ex);
}
}
private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
SimilarData.DataSource = ds.Tables["GameID"].DefaultView;
}
With that am getting this error.
ErrorSystem.InvalidOperationExceptoin:This Background worker is currently busyand and not run multiple tasks concurrently.
What do i do?
If you would be open to a simpler approach than using BackgroundWorker what I would suggest is making an async handler for the textbox.TextChanged event that takes note of the keystroke count going in before awaiting a "cooling off" period for rapid typing. If the count is the same before and after awaiting this delay it indicates that typing is sufficiently stable to perform a query.
I have mocked this out using sqlite-net-pcl for the sake of expediency. The DGV is bound to a DataSource that is a BindingList<Game>. The OnLoad override of MainForm initializes the DataGridView, then whatever database server you're using. The last thing is to subscribe to the TextChanged event and this is where all the where all the action takes place.
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
initDGV();
initSQL();
textBox.TextChanged += onTextBoxTextChanged;
}
BindingList<Game> DataSource = new BindingList<Game>();
// Comparing the awaited count to the total count so that
// the DGV isn't visually updated until all queries have run.
int _queryCount = 0;
TextBox.TextChanged event handler (async)
private async void onTextBoxTextChanged(object sender, EventArgs e)
{
if(string.IsNullOrWhiteSpace(textBox.Text))
{
return;
}
_queryCount++;
var queryCountB4 = _queryCount;
List<Game> recordset = null;
var captureText = textBox.Text;
// Settling time for rapid typing to cease.
await Task.Delay(TimeSpan.FromMilliseconds(250));
// If keypresses occur in rapid succession, only
// respond to the latest one after a settling timeout.
if (_queryCount.Equals(queryCountB4))
{
await Task.Run(() =>
{
using (var cnx = new SQLiteConnection(ConnectionString))
{
var sql = $"SELECT * FROM games WHERE GameID LIKE '{captureText}%'";
recordset = cnx.Query<Game>(sql);
}
});
DataSource.Clear();
foreach (var game in recordset)
{
DataSource.Add(game);
}
}
else Debug.WriteLine("Waiting for all pending queries to complete");
}
DataGridView
private void initDGV()
{
dataGridView.DataSource = DataSource;
dataGridView.AllowUserToAddRows = false;
DataSource.Add(new Game { GameID = "Generate Columns" });
dataGridView
.Columns[nameof(Game.GameID)]
.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
dataGridView
.Columns[nameof(Game.Created)]
.AutoSizeMode = DataGridViewAutoSizeColumnMode.AllCells;
DataSource.Clear();
}
Database
private void initSQL()
{
// For testing, start from scratch every time
if (File.Exists(ConnectionString)) File.Delete(ConnectionString);
using (var cnx = new SQLiteConnection(ConnectionString))
{
cnx.CreateTable<Game>();
cnx.Insert(new Game { GameID = "Awe" });
cnx.Insert(new Game { GameID = "Abilities" });
cnx.Insert(new Game { GameID = "Abscond" });
cnx.Insert(new Game { GameID = "Absolve" });
cnx.Insert(new Game { GameID = "Absolute" });
}
}
Game
[Table("games")]
class Game
{
[PrimaryKey, Browsable(false)]
public string Guid { get; set; } = System.Guid.NewGuid().ToString().ToUpper();
public string GameID { get; set; }
private DateTime _created = DateTime.Now.Date;
public string Created
{
get => _created.ToShortDateString();
set
{
if(DateTime.TryParse(value, out DateTime dt))
{
_created = dt.Date;
}
}
}
}

Implementing async/task/await in a multi-tier windows form application

I would appreciate some help with async/Task implementation in my Windows Form application.
In this app, I retrieve data from the datacenter SQl server database using WCF services, so part of this code runs on the client and some on the datacenter server where it retrieves the data and returns it. I’d like to optimize the code using async/Task on the clien or server or preferably both. The example code starts with a Windows Form with a button, when the button is clicked it gets a value from the database, displays it and updates a local variable.
I’m not clear if I can simply implement async/Task and Task.Run in the first button click event or whether the code should be cascaded through all methods, or something in between. I'm also not clear on how to handle the wcf service.
I’ve created a simplified example of the code, pretty close to sequentially.
In this code, the return value updates the windows form. I’d like to see how this code is optimized using async/Task await for this purpose and what would be different if the code did not return a value.
public partial class Form1 : Form
{
int returnvalue = 0;
public Form1()
{
InitializeComponent();
}
private void btnGetResult_Click(object sender, EventArgs e)
{
int rowcount = ChangeProductPrice(.05m);
txtResult.Text = rowcount.ToString();
}
private int ChangeProductPrice(decimal priceincrease)
{
int rv = MyData.WebServiceObject.ChangePrice(priceincrease);
UpdateLocalVariables(rv);
return rv;
}
private void UpdateLocalVariables(int rv)
{
returnvalue = rv;
}
}
public static class MyData
{
private static IMyDataWCFService _webserviceobject = null;
public static IMyDataWCFService WebServiceObject
{
get
{
if (_webserviceobject is null)
{
//...code to initialize it
}
return _webserviceobject;
}
}
}
[ServiceContract(SessionMode = SessionMode.Required)]
public interface IMyDataWCFService
{
[OperationContract]
int ChangePrice(decimal priceincrease);
}
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession, ConcurrencyMode = ConcurrencyMode.Multiple)]
public class MyDataWCFService : IMyDataWCFService
{
private MyDataService _serviceObject = null;
private MyDataService ServiceObject
{
get
{
if (_serviceObject == null)
{
_serviceObject = new MyDataService();
}
return _serviceObject;
}
}
public int ChangePrice(decimal priceincrease)
{
return ServiceObject.ChangePrice(priceincrease);
}
}
public class MyDataService //running on server
{
public int ChangePrice(decimal priceincrease)
{
int rows = 0;
SqlConnection conn = null;
try
{
conn = this.GetSqlConnection();
using (SqlCommand cmd = new SqlCommand())
{
cmd.CommandType = CommandType.StoredProcedure;
cmd.Connection = conn;
cmd.CommandText = "mysp_UpdatePrice";
cmd.Parameters.Add(new SqlParameter("#Rate", priceincrease));
conn.Open();
rows = cmd.ExecuteNonQuery();
}
}
catch (Exception ex)
{
ProcessError(ex);
}
finally
{
if (conn != null)
ReturnSqlConnection(conn);
}
return rows;
}
private SqlConnection GetSqlConnection()
{
//dostuff;
return new SqlConnection();
}
private void ProcessError(Exception ex)
{
//dostuff;
}
private void ReturnSqlConnection(SqlConnection conn)
{
//dostuff;
}
}

how to get string value from a method in class?

I have a class where I have a method that is creating a connection string.I want to access that connection string from other windows,but that isn't working.This is my code so far:
My main login is just calling the method from a class on a button click
private void button_Click(object sender, RoutedEventArgs e)
{
Class1 kl = new Class1();
kl.SQLCon(textBox, textBox_Copy);
}
My class looks like this:
class Class1
{
string user = string.Empty;
string pass = string.Empty;
private string ConString = string.Empty;
public string User { get { return this.ConString; } set { this.ConString = value; } }
public void SQLCon(TextBox tb1,TextBox tb2)
{
user = tb1.Text;
pass = tb2.Text;
if (string.IsNullOrWhiteSpace(user) || string.IsNullOrWhiteSpace(pass))
{
MessageBox.Show("Popuni sva polja");
return;
}
SqlConnection con;
SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder();
builder.DataSource = "DESKTOP";
builder.InitialCatalog = "Manager";
builder.IntegratedSecurity = false;
builder.UserID = user;
builder.Password = pass;
try
{
using (con = new SqlConnection(builder.ToString()))
{
con.Open();
if ((con != null && con.State == System.Data.ConnectionState.Open))
{
ConString = builder.ToString();
MessageBox.Show("Uspesno logovan!" + ConString);
Window1 win = new Window1();
win.ShowDialog();
}
}
}
catch (SqlException ex)
{
MessageBox.Show(ex.Message);
}
}
}
Now when from the Window1 I call the method to get ConString,I get nothing,blank.The problem is that it is not looking in the SQLCon for the string value,it is looking it as it is defined at the beggining of the class.
I can get this to work with this solution: before calling ShowDialog,I can have a string in the Window1, that I can pass this value, something like win.stringCon = ConString,and this works.But I want to be able to do this:
Window1 code:
private void button_Click(object sender, RoutedEventArgs e)
{
Class1 myClass = new Class1();
string conn = myClass.User;
MessageBox.Show(conn);
}
SOLUTION: You can't do it the way I imagined. But here is what you can do.
You define a string (conString) in the Window1 that will be you connection string.And you put this line before you call win.ShowDialog():
mw.conString = conString;
This way you have the string in your new window.And with one line you can transport the value from Window1 to some other Window that you will open next.

WCF Basic WinForm App Communication Issue

All, I have extended this tutorial to get and reverse string displayed in two seperate WinForm applications. However, the end goal is to get this working between to WinForm apps that pass SQL between eachother. To facilitate this I have extended this example and the following is what I have
A library .dll containing
public class WcfInterface
{
private static WcfInterface instance;
private ServiceHost host;
private const string serviceEnd = "Done";
protected WcfInterface()
{
}
public static WcfInterface Instance()
{
if (instance == null)
instance = new WcfInterface();
return instance;
}
public void OpenServiceHost<T, U>()
{
host = new ServiceHost(typeof(U), new Uri[] { new Uri("net.pipe://localhost") });
host.AddServiceEndpoint(typeof(T), new NetNamedPipeBinding(), serviceEnd);
host.Open();
}
public void CloseServiceHost<T>()
{
host.Close();
}
public T AddListnerToServiceHost<T>()
{
ChannelFactory<T> pipeFactory =
new ChannelFactory<T>(new NetNamedPipeBinding(),
new EndpointAddress(String.Format("net.pipe://localhost/{0}",
serviceEnd)));
T pipeProxy = pipeFactory.CreateChannel();
return pipeProxy;
}
}
So on the 'server' form, I do
private void Form1_Load(object sender, EventArgs e)
{
List<string> sqlList = new List<string>();
foreach (string line in this.richTextBoxSql.Lines)
sqlList.Add(line);
SqlInfo sqlInfo = new SqlInfo(sqlList);
WcfInterface wcfInterface = WcfInterface.Instance();
wcfInterface.OpenServiceHost<ISqlListing, SqlInfo>();
}
Where
public class SqlInfo : ISqlListing
{
private List<string> sqlList;
public SqlInfo(List<string> sqlList)
{
this.sqlList = sqlList;
}
public List<string> PullSql()
{
return sqlList;
}
}
[ServiceContract]
public interface ISqlListing
{
[OperationContract]
List<string> PullSql();
}
In the client WinForm app
private ISqlListing pipeProxy;
public Form1()
{
InitializeComponent();
WcfInterface wcfInterface = WcfInterface.Instance();
pipeProxy = wcfInterface.AddListnerToServiceHost<ISqlListing>();
}
and on the click event I attampt to get the List<string> from the server
private void button1_Click(object sender, EventArgs e)
{
this.richTextBoxSql.Text = pipeProxy.PullSql().ToString(); // Code hangs here.
}
My question is what is wrong with this?
Thanks for your time.
Edit. I have now also changed the client code according to comments as follows
private ISqlListing pipeProxy { get; set; }
private const string serviceEnd = "Done";
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
this.richTextBoxSql.Text = pipeProxy.PullSql().ToString();
}
private void Form1_Load(object sender, EventArgs e)
{
ChannelFactory<ISqlListing> pipeFactory =
new ChannelFactory<ISqlListing>(
new NetNamedPipeBinding(),
new EndpointAddress(
String.Format("net.pipe://localhost/{0}", serviceEnd)));
pipeProxy = pipeFactory.CreateChannel();
}
this also hangs on the click event.
The way you have the code set up, you are creating a WCF server on the client by referencing WcfInterface.Instance. You are then calling it from the same thread that it is being served on, causing your application to lock up.
There are a number of ways to get around this. Here are a few that come to mind:
Get the service running in your first WinForm app, then use the "Add Service Reference" functionality in visual studio to create your proxies. Note that you'll have to
You can still reference a common library for the WCF contracts, but rework your code so that you're not creating an instance of the service in your "client" WinForms app.

Access TextBox from Static method in C#?

I have 2 classes: MyForm and Database
In MyForm I have a method to change a label text to show error:
public void printError(string text){
label1.Text = text;
}
My Database class needs to access that method too, so I make it static:
public static void printError(MyForm form, string text){
form.label1.Text = text;
}
Now the problem is, how do I call that method from Database class?
This question I found said that I need to pass MyForm into Database's contructor like this:
class MyForm : Form{
Database db;
public Form(){
db = new Database(this);
}
}
class Database{
MyForm form;
public Database(MyForm f){
form = f;
}
...
//then I can access the printError like this
MyForm.printError(form, "You got error");
}
I tried that and it freezes the form. Any other solution?
Thanks
Here is a very simple example of how you can achieve this without your data layer knowing about your UI:
class MyForm : Form
{
Database db;
public Form()
{
db = new Database(this);
}
public void DoSomething()
{
var errors = db.Login("", "");
if (errors.Any())
label1.Text = errors.First(); // Or you can display all all of them
}
}
class Database
{
public List<string> Login(string username, string password)
{
var errors = new List<string>();
if (string.IsNullOrEmpty(username))
errors.Add("Username is required");
if (string.IsNullOrEmpty(password))
errors.Add("Password is required");
[...]
return errors;
}
}
Like #Matthew Ferreira and others have stated the design is not idea, but here's something to get you started.
class MyForm : Form
{
public void SomeMethod()
{
var dataAccess = new Repository();
dataAccess.ExecuteQuery();
if (dataAccess.Exceptions.Any())
{
// display your error messages
form.label1.Text = dataAccess.Exceptions.Select(x => x.ToString());
}
}
}
class Repository
{
private readonly HashSet<Exception> _exceptions = new HashSet<Exception>();
public IEnumerable<Exception> Exceptions
{
get { return _exceptions; }
}
public int ExecuteQuery()
{
var numberOfRecordsAffected = 0;
try
{
// do something
}
catch (Exception ex)
{
// normall catching exceptions is a bad idea
// and you should really catch the exception at the
// layer best equiped to deal with it
_exceptions.Add(ex);
}
// but, for the purpose of this example we might want to add some logic to try the query on another database ????
try
{
// do something
}
catch (Exception ex)
{
_exceptions.Add(ex);
}
return numberOfRecordsAffected;
}
}
You need to look up "seperation of concerns". Its really bad to mix your UI code with you Database Access Layer (DAL). Better to bind the UI to business objects that are populated via a DAL.
To let the UI know about an error you could simply use a delegate.
namespace OperationErrorDelegate
{
public delegate void OperationErrorHandler(Exception ex);
public class DAL
{
public event OperationErrorHandler ReportError;
public void DoDALOperationThatCausesError()
{
try
{
int i = 1;
int j = 0;
int k = i/j;
}
catch (Exception ex)
{
ReportError(ex);
}
}
}
}
Add this code to the form:
using System ;
using System.Windows.Forms;
namespace OperationErrorDelegate
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
DAL DAL = new DAL();
DAL.ReportError += new OperationErrorHandler(DAL_OperationErrorProgress);
DAL.DoDALOperationThatCausesError();
}
private void DAL_OperationErrorProgress(Exception ex)
{
label1.Text = ex.Message;
}
}
}
Assuming the OP's requirement is to display an error message in a label, when the credentials are wrong:
private void btn_login_Click(object sender, EventArgs e)
{
MySqlConnection con = new MySqlConnection("server=localhost;uid=root;password=abc;database=mydb");
MySqlCommand cmd = new MySqlCommand("select * from emp where name='" + textBox1.Text + "'and pwd='" + textBox2.Text + "'",con);
con.Open();
MySqlDataReader dr = cmd.ExecuteReader();
if (dr.Read())
{ //successful
//navigate to next page or whatever you want
}
else
Label1.Text("Invalid userid or password");
con.Close();
}
And if you need error message for wrong data type (the user input string but the database column is Integer), then use validations at client side. You dont need to do it at backend, since that will be a burden.
You can use regular expressions for that in the button_click itself.

Categories